diff --git a/website/AGENTS.md b/website/AGENTS.md index b216e01f9..04d368430 100644 --- a/website/AGENTS.md +++ b/website/AGENTS.md @@ -16,8 +16,9 @@ website/ layout.ts root layout (head, OG/Twitter metadata, header/footer chrome, Tailwind tokens) page.ts / → the entire one-page landing site. - Hero, features grid, code samples, agent - badges, and footer all live here. + Hero, features grid, code samples, and agent + badges all live here. The header and footer + chrome are in layout.ts (shared by every page). changelog/page.ts /changelog. Reads ../../../changelog//*.md at SSR time and renders the unified release feed. The deployment image must include the diff --git a/website/app/compare/page.ts b/website/app/compare/page.ts index c1752daf9..3ccb031d1 100644 --- a/website/app/compare/page.ts +++ b/website/app/compare/page.ts @@ -33,7 +33,7 @@ export default async function Compare() {
- WebJs vs ${c.competitor} + WebJs vs ${c.competitor}

${c.tagline}

${c.description}

diff --git a/website/app/layout.ts b/website/app/layout.ts index 3c91fb4b1..2f7c601c9 100644 --- a/website/app/layout.ts +++ b/website/app/layout.ts @@ -1,6 +1,7 @@ import { html, cspNonce } from '@webjsdev/core'; import '#components/theme-toggle.ts'; import { DOCS_URL, UI_URL, EXAMPLE_BLOG_URL, GH_URL, NEW_TAB } from '#lib/links.ts'; +import { siteFooter } from '#lib/site-footer.ts'; /** * Root layout for the redesigned marketing site. @@ -286,8 +287,10 @@ export default function RootLayout({ children }: { children: unknown }) { -
+
${children} + + ${siteFooter()}
`; } diff --git a/website/app/page.ts b/website/app/page.ts index 71ed19c02..c0ebf3c6f 100644 --- a/website/app/page.ts +++ b/website/app/page.ts @@ -3,7 +3,6 @@ import '#components/copy-cmd.ts'; import '#components/like-button.ts'; import { COMPONENT_SAMPLE, ACTION_SAMPLE, PAGE_SAMPLE } from '#lib/samples.ts'; import { DOCS_URL, GH_URL, NEW_TAB } from '#lib/links.ts'; -import { siteFooter } from '#lib/site-footer.ts'; // highlight() runs only at SSR (codeWindow renders its output into the served // HTML), but it does ship to the client as a small dead module: the page loads // in the browser to register copy-cmd, and that pulls in its @@ -400,7 +399,5 @@ middleware.ts - - ${siteFooter()} `; } diff --git a/website/app/why/page.ts b/website/app/why/page.ts index 56fcea097..581163900 100644 --- a/website/app/why/page.ts +++ b/website/app/why/page.ts @@ -1,7 +1,6 @@ import { html } from '@webjsdev/core'; import '#components/copy-cmd.ts'; import { DOCS_URL, GH_URL, EXAMPLE_BLOG_URL, NEW_TAB } from '#lib/links.ts'; -import { siteFooter } from '#lib/site-footer.ts'; /** * /why @@ -212,7 +211,5 @@ Counter.register('counter'); - - ${siteFooter()} `; } diff --git a/website/lib/site-footer.ts b/website/lib/site-footer.ts index cd6b66342..01d5bb53a 100644 --- a/website/lib/site-footer.ts +++ b/website/lib/site-footer.ts @@ -2,11 +2,12 @@ import { html } from '@webjsdev/core'; import { DOCS_URL, UI_URL, EXAMPLE_BLOG_URL, GH_URL, DISCORD_URL, NEW_TAB } from '#lib/links.ts'; /** - * The site-wide footer, shared across marketing pages (the home page and /why). + * The site-wide footer, rendered once by the root layout (app/layout.ts) so it + * appears on every page, the same way the header/nav does. * - * It lives here rather than inline in a page so every page renders the same - * chrome. Pure SSR-time helper: it returns an `html` fragment and touches no - * client globals, so importing it never ships a page to the browser. + * It lives here rather than inline in the layout so the chrome stays readable. + * Pure SSR-time helper: it returns an `html` fragment and touches no client + * globals, so importing it never ships a page to the browser. * * Anchor links point at `/#` (not a bare `#`) so a section anchor * resolves from any page, not only the home page. diff --git a/website/test/ssr/layout-ssr.test.ts b/website/test/ssr/layout-ssr.test.ts index d2ae7a417..9069f725c 100644 --- a/website/test/ssr/layout-ssr.test.ts +++ b/website/test/ssr/layout-ssr.test.ts @@ -54,6 +54,18 @@ test('every nav landmark carries a distinguishing aria-label', async () => { assert.ok(labels.includes('Footer'), 'the footer nav is labeled Footer'); }); +test('the root layout renders the site footer around a bare child (every page gets it)', async () => { + // The footer is layout chrome, not per-page markup, so a page that renders + // only its own
(blog, compare, changelog, articles) still gets exactly + // one footer. Compose the layout around a minimal child and assert the footer + // nav is present and appears once. Guards against the footer regressing back + // into individual pages (where blog/compare/changelog silently lost it). + const out = await renderToString(RootLayout({ children: html`
content
` })); + const footers = (out.match(/ { const out = await renderToString(RootLayout({ children: LandingPage() })); // Every target="_blank" link carries a visually-hidden new-tab cue.