From e52345c79f89fbc8afba7cea8fa00b2a6eb82ef1 Mon Sep 17 00:00:00 2001
From: Muhammad
Date: Fri, 6 Sep 2024 22:04:09 +0700
Subject: [PATCH 1/4] test: add test to `examples/solid-query`
---
examples/solid-query/.test-dev.test.ts | 2 +
examples/solid-query/.test-preview.test.ts | 2 +
examples/solid-query/.testRun.ts | 70 +++++++++++++++++++++
examples/solid-query/pages/index/Movies.tsx | 2 +-
4 files changed, 75 insertions(+), 1 deletion(-)
create mode 100644 examples/solid-query/.test-dev.test.ts
create mode 100644 examples/solid-query/.test-preview.test.ts
create mode 100644 examples/solid-query/.testRun.ts
diff --git a/examples/solid-query/.test-dev.test.ts b/examples/solid-query/.test-dev.test.ts
new file mode 100644
index 0000000..f77ae53
--- /dev/null
+++ b/examples/solid-query/.test-dev.test.ts
@@ -0,0 +1,2 @@
+import { testRun } from "./.testRun";
+testRun("pnpm run dev");
diff --git a/examples/solid-query/.test-preview.test.ts b/examples/solid-query/.test-preview.test.ts
new file mode 100644
index 0000000..ed102ea
--- /dev/null
+++ b/examples/solid-query/.test-preview.test.ts
@@ -0,0 +1,2 @@
+import { testRun } from "./.testRun";
+testRun("pnpm run preview");
diff --git a/examples/solid-query/.testRun.ts b/examples/solid-query/.testRun.ts
new file mode 100644
index 0000000..6dd5ca1
--- /dev/null
+++ b/examples/solid-query/.testRun.ts
@@ -0,0 +1,70 @@
+export { testRun };
+
+import { test, expect, run, page, partRegex, getServerUrl, autoRetry } from "@brillout/test-e2e";
+const dataHk = partRegex`data-hk=${/[0-9-]+/}`;
+
+function testRun(cmd: `pnpm run ${"dev" | "preview"}`) {
+ run(cmd);
+
+ const content = "Return of the Jedi";
+ const loading = "Loading movies...";
+ const titleDefault = "My Vike + Solid App";
+ const titleOverriden = "6 Star Wars movies";
+ const titleAsScript = ``;
+ const description = partRegex``;
+ test("HTML (as user)", async () => {
+ const html = await fetchAsUser("/");
+ expect(html).toContain(content);
+ expect(html).toContain(loading);
+ expect(html).toContain(titleAsScript);
+ expect(getTitle(html)).toBe(titleDefault);
+ expect(html.split("").length).toBe(2);
+ expect(html).not.toMatch(description);
+ });
+ test("HTML (as bot)", async () => {
+ const html = await fetchAsBot("/");
+ expect(html).toContain(content);
+ expect(html).not.toContain(loading);
+ expect(html).not.toContain(titleAsScript);
+ expect(getTitle(html)).toBe(titleOverriden);
+ expect(html.split("").length).toBe(2);
+ expect(html).toMatch(description);
+ });
+ test("DOM", async () => {
+ await page.goto(getServerUrl() + "/");
+ const body = await page.textContent("body");
+ // Playwright seems to await the HTML stream
+ expect(body).not.toContain(loading);
+ expect(body).toContain(content);
+ await testCounter();
+ });
+}
+
+function getTitle(html: string) {
+ const title = html.match(/(.*?)<\/title>/i)?.[1];
+ return title;
+}
+
+async function testCounter() {
+ // autoRetry() for awaiting client-side code loading & executing
+ await autoRetry(
+ async () => {
+ expect(await page.textContent("button")).toBe("Counter 0");
+ await page.click("button");
+ expect(await page.textContent("button")).toContain("Counter 1");
+ },
+ { timeout: 5 * 1000 },
+ );
+}
+
+async function fetchAsBot(pathname: string) {
+ return await fetchHtml(pathname, "curl/8.5.0");
+}
+async function fetchAsUser(pathname: string) {
+ return await fetchHtml(pathname, "chrome");
+}
+async function fetchHtml(pathname: string, userAgent: string) {
+ const response = await fetch(getServerUrl() + pathname, { headers: { ["User-Agent"]: userAgent } });
+ const html = await response.text();
+ return html;
+}
diff --git a/examples/solid-query/pages/index/Movies.tsx b/examples/solid-query/pages/index/Movies.tsx
index 07713d1..52cf93b 100644
--- a/examples/solid-query/pages/index/Movies.tsx
+++ b/examples/solid-query/pages/index/Movies.tsx
@@ -17,7 +17,7 @@ export function Movies() {
};
return (
- Loading movies ...
}>
+
{(movies) => (
<>
From 72b20ec699ee6dd53dc0ba4333bc3bc614f18795 Mon Sep 17 00:00:00 2001
From: Muhammad
Date: Fri, 6 Sep 2024 23:51:36 +0700
Subject: [PATCH 2/4] fix(useConfig): add support for `useConfig()` with HTML
streaming
---
.../hooks/useConfig/useConfig-server.ts | 19 +++++++++++++++++--
.../vike-solid/integration/onRenderHtml.tsx | 14 +++++++++++---
packages/vike-solid/types/Config.ts | 1 +
packages/vike-solid/types/PageContext.ts | 3 ++-
4 files changed, 31 insertions(+), 6 deletions(-)
diff --git a/packages/vike-solid/hooks/useConfig/useConfig-server.ts b/packages/vike-solid/hooks/useConfig/useConfig-server.ts
index 04464d5..056cfda 100644
--- a/packages/vike-solid/hooks/useConfig/useConfig-server.ts
+++ b/packages/vike-solid/hooks/useConfig/useConfig-server.ts
@@ -1,7 +1,7 @@
export { useConfig };
import type { PageContext } from "vike/types";
import type { PageContextInternal } from "../../types/PageContext.js";
-import type { ConfigFromHook } from "../../types/Config.js";
+import type { ConfigFromHook, Stream } from "../../types/Config.js";
import { usePageContext } from "../usePageContext.js";
import { getPageContext } from "vike/getPageContext";
import { objectKeys } from "../../utils/objectKeys.js";
@@ -20,7 +20,14 @@ function useConfig(): (config: ConfigFromHook) => void {
// Component
pageContext = usePageContext();
- return (config: ConfigFromHook) => setPageContextConfigFromHook(config, pageContext);
+ return (config: ConfigFromHook) => {
+ if (!pageContext._headAlreadySet) {
+ setPageContextConfigFromHook(config, pageContext);
+ } else {
+ // already sent to the browser => send DOM-manipulating scripts during HTML streaming
+ apply(config, pageContext._stream!);
+ }
+ };
}
const configsClientSide = ["title"];
@@ -44,3 +51,11 @@ function setPageContextConfigFromHook(config: ConfigFromHook, pageContext: PageC
}
});
}
+
+function apply(config: ConfigFromHook, stream: Stream) {
+ const { title } = config;
+ if (title) {
+ const htmlSnippet = ``;
+ stream.write(htmlSnippet);
+ }
+}
diff --git a/packages/vike-solid/integration/onRenderHtml.tsx b/packages/vike-solid/integration/onRenderHtml.tsx
index aef1b9b..79c907a 100644
--- a/packages/vike-solid/integration/onRenderHtml.tsx
+++ b/packages/vike-solid/integration/onRenderHtml.tsx
@@ -40,16 +40,24 @@ const onRenderHtml: OnRenderHtmlAsync = async (
`;
};
-function getPageHtml(pageContext: PageContextServer) {
+function getPageHtml(pageContext: PageContextServer & PageContextInternal) {
let pageHtml: string | ReturnType | TPipe = "";
if (pageContext.Page) {
if (!pageContext.config.stream) {
pageHtml = dangerouslySkipEscape(renderToString(() => getPageElement(pageContext)));
} else if (pageContext.config.stream === "web") {
- pageHtml = renderToStream(() => getPageElement(pageContext)).pipeTo;
+ pageHtml = renderToStream(() => getPageElement(pageContext), {
+ onCompleteShell(info) {
+ pageContext._stream ??= info;
+ },
+ }).pipeTo;
stampPipe(pageHtml, "web-stream");
} else {
- pageHtml = renderToStream(() => getPageElement(pageContext)).pipe;
+ pageHtml = renderToStream(() => getPageElement(pageContext), {
+ onCompleteShell(info) {
+ pageContext._stream ??= info;
+ },
+ }).pipe;
stampPipe(pageHtml, "node-stream");
}
}
diff --git a/packages/vike-solid/types/Config.ts b/packages/vike-solid/types/Config.ts
index ca4b84b..14250b1 100644
--- a/packages/vike-solid/types/Config.ts
+++ b/packages/vike-solid/types/Config.ts
@@ -186,3 +186,4 @@ export type ConfigFromHook = PickWithoutGetter<
>;
export type ConfigFromHookResolved = Omit &
Pick;
+export type Stream = { write: (v: string) => void };
diff --git a/packages/vike-solid/types/PageContext.ts b/packages/vike-solid/types/PageContext.ts
index 14cb4d3..7ba5ab9 100644
--- a/packages/vike-solid/types/PageContext.ts
+++ b/packages/vike-solid/types/PageContext.ts
@@ -1,5 +1,5 @@
import type { JSX } from "solid-js";
-import type { ConfigFromHookResolved } from "./Config";
+import type { ConfigFromHookResolved, Stream } from "./Config";
// https://vike.dev/pageContext#typescript
declare global {
@@ -15,4 +15,5 @@ declare global {
export type PageContextInternal = {
_configFromHook?: ConfigFromHookResolved;
_headAlreadySet?: boolean;
+ _stream?: Stream;
};
From 711742cae563fc888b064b8377a4c6a84d0b042d Mon Sep 17 00:00:00 2001
From: Muhammad
Date: Sat, 7 Sep 2024 00:04:47 +0700
Subject: [PATCH 3/4] feat: add support for checking crawlers/bots and
integrate `renderToStringAsync()`
- Implemented detection for crawlers and bots
- Updated rendering process to use `renderToStringAsync()` when accessed by crawlers/bots for improved SEO
---
.../vike-solid/integration/onRenderHtml.tsx | 17 +++++++++++++----
packages/vike-solid/package.json | 1 +
packages/vike-solid/types/isBot.d.ts | 4 ++++
pnpm-lock.yaml | 9 +++++++++
4 files changed, 27 insertions(+), 4 deletions(-)
create mode 100644 packages/vike-solid/types/isBot.d.ts
diff --git a/packages/vike-solid/integration/onRenderHtml.tsx b/packages/vike-solid/integration/onRenderHtml.tsx
index 79c907a..10fe525 100644
--- a/packages/vike-solid/integration/onRenderHtml.tsx
+++ b/packages/vike-solid/integration/onRenderHtml.tsx
@@ -1,5 +1,5 @@
// https://vike.dev/onRenderHtml
-import { generateHydrationScript, renderToStream, renderToString } from "solid-js/web";
+import { generateHydrationScript, renderToStream, renderToString, renderToStringAsync } from "solid-js/web";
import { dangerouslySkipEscape, escapeInject, stampPipe } from "vike/server";
import { getHeadSetting } from "./getHeadSetting.js";
import { getPageElement } from "./getPageElement.js";
@@ -10,6 +10,7 @@ import type { PageContextInternal } from "../types/PageContext.js";
import type { Head } from "../types/Config.js";
import type { JSX } from "solid-js/jsx-runtime";
import { isCallable } from "../utils/isCallable.js";
+import isBot from "isbot-fast";
export { onRenderHtml };
@@ -18,7 +19,7 @@ type TPipe = Parameters[0];
const onRenderHtml: OnRenderHtmlAsync = async (
pageContext: PageContextServer & PageContextInternal,
): ReturnType => {
- const pageHtml = getPageHtml(pageContext);
+ const pageHtml = await getPageHtml(pageContext);
const headHtml = getHeadHtml(pageContext);
@@ -40,10 +41,18 @@ const onRenderHtml: OnRenderHtmlAsync = async (