diff --git a/docs/guide/pages-routing/markdown.md b/docs/guide/pages-routing/markdown.md
index 2d4c4b593..52718ae90 100644
--- a/docs/guide/pages-routing/markdown.md
+++ b/docs/guide/pages-routing/markdown.md
@@ -137,7 +137,7 @@ Welcome to Void. Here's an interactive demo:
```
- **`';
+
type IslandRenderer = {
renderIslandPage(
page: { component: string; props: Record },
@@ -49,8 +54,56 @@ export default function Layout({ children }) {
);
expect(html).toContain('Hello from frontmatter
');
expect(html).toContain('Body');
+ // No island on the page, so the frontmatter stays on the server.
+ expect(html).not.toContain('__void_frontmatter');
} finally {
await server.close();
rmSync(root, { recursive: true, force: true });
}
});
+
+it('an island reads the same frontmatter in the browser as on the server', async () => {
+ const root = mkdtempSync(join(import.meta.dirname, '__tmp_md_island_frontmatter_'));
+ mkdirSync(join(root, 'pages'));
+ mkdirSync(join(root, 'components'));
+ writeFileSync(
+ join(root, 'pages', 'layout.island.tsx'),
+ `import Title from "../components/Title" with { island: "load" };
+export default function Layout({ children }) {
+ return {children}
;
+}`,
+ );
+ writeFileSync(
+ join(root, 'components', 'Title.tsx'),
+ `import { createElement } from "react";
+import { useFrontmatter } from "@void/md";
+export default function Title() {
+ return createElement("p", { id: "island-fm" }, String(useFrontmatter().title));
+}`,
+ );
+ writeFileSync(
+ join(root, 'pages', 'index.md'),
+ '---\ntitle: Hello from frontmatter\n---\n\n# Body\n',
+ );
+
+ let visit: IslandPageVisit | undefined;
+ try {
+ visit = await openIslandPage(root, {
+ alias: { '@void/react': join(import.meta.dirname, '../../src/runtime/index.ts') },
+ plugins: [
+ ...voidMarkdown(),
+ { name: 'void-react:pages' },
+ ...islandsPlugin(join(root, 'pages')),
+ ],
+ });
+ expect(visit.html).toContain(ISLAND_FRONTMATTER_SCRIPT);
+ await waitForIslandsHydrated(visit.page);
+ expect(await visit.page.textContent('#island-fm')).toBe('Hello from frontmatter');
+ // A server/browser mismatch makes React log a hydration error and re-render the island.
+ expect(visit.diagnostics().toLowerCase()).not.toContain('hydrat');
+ expect(visit.diagnostics()).not.toContain('pageerror');
+ } finally {
+ await visit?.close();
+ rmSync(root, { recursive: true, force: true });
+ }
+});
diff --git a/packages/solid/src/plugin-islands.ts b/packages/solid/src/plugin-islands.ts
index 3b6e97ce8..4147f4e41 100644
--- a/packages/solid/src/plugin-islands.ts
+++ b/packages/solid/src/plugin-islands.ts
@@ -258,6 +258,7 @@ import { renderToStringAsync, generateHydrationScript } from "solid-js/web";
import { createComponent } from "solid-js";
import { SharedContext, ErrorsContext, ParamsContext, FrontmatterContext } from "@void/solid";
import { renderHeadToString, renderHtmlAttrs, renderBodyAttrs } from "void/pages-head";
+import { islandFrontmatterScript } from "void/pages-server";
${generateComponentManifest(scan, pagesDir)}
${mdScriptIdsCode}
@@ -311,6 +312,11 @@ export async function renderIslandPage(pageObj, assetTags) {
const html = await renderToStringAsync(() => createComponent(ServerApp, {}));
+ // Islands hydrate as separate roots. Send the page frontmatter so their
+ // useFrontmatter() returns the same value in the browser.
+ const __frontmatterScript = __pagesNeedingClientJS.has(pageObj.component) && html.includes("data-island=")
+ ? islandFrontmatterScript(__pageModule.frontmatter)
+ : "";
const __mdScriptAttr = __mdClientScriptIds.has(pageObj.component) ? \` data-md-script="\${escapeAttr(pageObj.component)}"\` : "";
const headHtml = pageObj.head ? renderHeadToString(pageObj.head) : "";
const htmlAttrs = pageObj.head ? renderHtmlAttrs(pageObj.head) : "";
@@ -321,7 +327,7 @@ export async function renderIslandPage(pageObj, assetTags) {
\${headHtml}\${assetTags.css}\${__pagesNeedingClientJS.has(pageObj.component) ? assetTags.preloads : ""}\${__pagesNeedingClientJS.has(pageObj.component) ? generateHydrationScript() : ""}
\${html}
- \${__pagesNeedingClientJS.has(pageObj.component) ? assetTags.body : ""}
+ \${__frontmatterScript}\${__pagesNeedingClientJS.has(pageObj.component) ? assetTags.body : ""}