Skip to content

Migrate website to Nextra 4#2132

Draft
mnapoli wants to merge 20 commits into
masterfrom
nextra-v4
Draft

Migrate website to Nextra 4#2132
mnapoli wants to merge 20 commits into
masterfrom
nextra-v4

Conversation

@mnapoli

@mnapoli mnapoli commented Jul 4, 2026

Copy link
Copy Markdown
Member

Migrates the website from Nextra 2 (Pages Router) to Nextra 4 (App Router). Part of #2130.

This first phase stands up a green-building v4 skeleton; content is ported incrementally in later phases.

Phase checklist

  • Scaffold + config: Nextra 4.6.1 + nextra-theme-docs 4.6.1, next@15, react@19, tailwind v4 (@tailwindcss/postcss + forms), zod@4.3.0 override, pagefind. Removed next-seo, Algolia DocSearch, autoprefixer.
  • App Router: app/layout.jsx (all theme.config options ported), app/[[...mdxPath]] catch-all, mdx-components.jsx, app/api/md/[...slug] route handler.
  • Metadata: title template %s – Bref, absolute home title, frontmatter description, OG/social-card, twitter, google-site-verification, per-docs .md alternate link.
  • Hash-redirect port: _app.jsx client redirect logic → 'use client' HashRedirects component.
  • Styles: Tailwind v4 CSS-first (@theme blue palette, --nextra-primary-hue: 202deg).
  • Build green: npm run build passes; sitemap + pagefind postbuild chain runs.
  • Bulk content sweep (86 pages): Tab→Tabs.Tab, Card→Cards.Card, NextSeo→frontmatter across all docs/ MDX; MDX v3 blank-line fixes; fixed relative imports into website/src/components/; extracted the interactive costs calculator to a 'use client' component.
  • _meta.json_meta.js for all 13 docs/ files.
  • Full sync activated: all of docs/ (67 mdx + 3 md + assets) now syncs into website/content/docs; build produces 70 /docs/* routes, matching every /docs/* URL in the current live sitemap.
  • Data pages (index/credits/changelog) → async Server Components: home invocations + credits sponsors/contributors + X-Ray changelog fetch directly on the server, keeping try/catch fallbacks.
  • Marketing/checkout pages (cloud/sentry/xray/support/news/404): hooks-driven bodies extracted to 'use client' components; page bodies stay server. 404 → app/not-found.jsx with dynamicParams=false so unknown URLs hit the custom boundary.
  • URL parity: new build's sitemap matches the live bref.sh sitemap exactly — 91/91 URLs, zero missing, zero extra (the six accidental /cloud/* component routes were recreated for parity).
  • Cleanup: website/_port/ deleted; every page now lives under content/ or app/.
  • Pagefind docs-only exclusion (Phase D): postbuild now runs pagefind --glob "{docs.html,docs/**/*.html}" — route-based scoping so new marketing pages can'''t leak into search without needing per-page annotations. Verified 70/70 indexed fragment URLs are all /docs/*.
  • Phase D plumbing verified on the built app (next start): .html strip redirects, redirects.js path + hash entries (hash entries confirmed via static analysis — HashRedirects.jsx shares the same redirects.js object, so it inherently covers exactly the hash-bearing keys since non-hash ones are already resolved server-side), Accept: text/markdown negotiation, /docs/*.md routes (incl. nested /docs/deploy/aws-cdk.md), sitemap (91/91 URLs, correct host, no localhost//api/_pagefind leakage), robots.txt (matches live), Plausible proxy + provider wiring (matches live config), and trailing-slash behavior (matches live).
  • Fixed a bug found during verification: /api/md route leaked a raw JSX comment from content/docs/deploy/aws-cdk.mdx (recently inlined) into crawler-facing Markdown output — now stripped.

Review fixes

  • Fixed /docs landing page emitting a broken /docs/.md alternate link (guarded to real docs pages only)
  • Hardened the markdown route (/api/md) against path traversal by rejecting resolved paths outside content/docs
  • Markdown route now falls back to .md source files when .mdx doesn't exist (fixes 3 pages that 404'd on their markdown endpoint)
  • Added /docs.md support (serves the docs index through the markdown route; route converted to an optional catch-all)
  • Fixed dead /docs/web-apps(/index) redirect (was keyed with a hash that never reaches the server) and added a working /docs/web-apps redirect
  • Filtered hash-anchor entries out of next.config redirects() since the server never sees the hash; they remain in redirects.js for the client-side HashRedirects component

https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR

Visual fixes

  • Fixed hero <h1> line-height collapse causing overlapping text on home + Bref Cloud (desktop and mobile)
  • Fixed Tailwind not scanning gitignored content/docs, which dropped doc-only utility classes (e.g. maturity-matrix colored dots)
  • Removed the docs "Copy page" button leaking onto marketing pages (home, cloud + its /cloud/* fragment routes, support, sentry, xray)
  • Fixed credits page rendering with docs sidebar/TOC/copy-page/feedback chrome instead of a clean marketing layout
  • Verified the news article dashboard image (suspected empty box) renders correctly — was a screenshot lazy-load timing artifact, not a bug
  • Restored v2 look for 2 maintainer-approved drift items: code blocks' light-blue background tint, and the search placeholder text ("Search docs"). All other drift (content width, callout colors, external-link arrows, sidebar active style) intentionally kept as v4 defaults.

mnapoli added 3 commits July 4, 2026 19:22
Replace the Nextra 2 / Pages Router config with a Nextra 4 scaffold:
next.config.mjs (ESM, plausible proxy + redirects.js + .md rewrite preserved),
Tailwind v4 CSS-first globals.css (blue palette + primaryHue as CSS vars),
@tailwindcss/postcss, and pinned next@15 + zod@4.3.0 per the migration spike.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Port every theme.config.jsx option into app/layout.jsx (AnimatedLogo navbar,
projectLink, Slack chatIcon, v3 Banner, footer, forcedTheme light, metadata
title template + OG + verification), add the [[...mdxPath]] catch-all with
frontmatter->metadata and per-docs .md alternate, the app/api/md route handler,
and mdx-components.jsx h1 override. Port _app.jsx hash-redirect logic to a
'use client' HashRedirects component and move middleware.js to the app root.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Add content/ with a home stub, root/docs _meta.js, and four sample docs pages
(plain, Tabs+Callout, and two real pages with NextSeo->frontmatter fixes) to
prove rendering. Move src/pages to _port/pages (staging, outside the build) and
point the Makefile sync + gitignore at content/docs. Full content sweep is the
next phase; sample docs are force-added over the content/docs ignore rule.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
mnapoli added 17 commits July 4, 2026 19:40
…MDX v3 blank lines)

Convert Tab -> Tabs.Tab and Card -> Cards.Card across all docs MDX (v4 renamed
these to sub-components of Tabs/Cards). Remove next-seo imports and <NextSeo>
tags, moving their description/title into YAML frontmatter (MDX v3 reads
frontmatter natively; page title otherwise falls back to the first heading).
Add a blank line between the last import and content where MDX v3's acorn
parser requires it. Fix relative imports into website/src/components/ (the
docs/ -> content/docs/ move sits one directory level shallower under
website/ than the old src/pages/docs/ did). Extract the interactive costs
calculator out of serverless-costs.mdx into a 'use client' component
(website/src/components/CostsCalculator.jsx) since inline `export function`
in MDX is not supported by Nextra v4.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Nextra v4 expects _meta.js exporting a default object instead of _meta.json;
JSON is valid as the object literal body, so this is a mechanical wrap. These
get synced into website/content/docs/ by the Makefile.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Follow-up to the _meta.js conversion: delete the old .json files now that
their .js replacements are in place.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
website/content/docs/_meta.js (the full ~30-entry sidebar config, including
separators) now comes entirely from the synced+converted docs/_meta.js
instead of Phase A's 5-entry placeholder. Root content/_meta.js keeps the
remaining _port/pages/_meta.json entries (news, cloud, support, credits,
404, sentry, xray) as commented-out Phase C notes so the nav doesn't link to
pages that don't exist yet.

All 86 docs/ files (67 mdx + 3 md + 13 _meta.js + images) now sync into
website/content/docs via `make content/docs`, and `npm run build` produces
70 /docs/* routes, matching every /docs/* URL in the current live sitemap
(https://bref.sh/sitemap.xml).

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Home invocations counter and credits sponsors/contributors become async
Server Components fetching directly (getBrefInvocations/getSponsors/getContributors),
replacing v2 getStaticProps + useData. Fetches keep their try/catch fallbacks so
builds succeed without AWS/GitHub credentials.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Extract the hooks-driven page body into a 'use client' BrefCloud component and
mark the interactive pricing/how-it-works subcomponents 'use client'. Recreate
the six /cloud/* routes (case-studies, faq, features, hero-bg, how-it-works,
pricing) that the v2 Pages Router exposed per-component, to preserve URL parity.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Extract the useState-driven landing pages into 'use client' SentryPage/XRayPage
components; docs/terms stay as MDX content routes. The X-Ray changelog (a React
page with getStaticProps in v2) becomes an async Server Component fetching
getReleases, rendered via a thin MDX wrapper. Page images live with their
components; docs screenshots live beside the content routes.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
News index + three articles (typesetting:article, ArticleHeader) and the support
page port as content pages; 404 becomes app/not-found.jsx. Set dynamicParams=false
on the catch-all so unknown URLs render the custom 404 boundary. Complete
content/_meta.js with all top-nav and hidden pages, and delete the _port staging
tree now that every page lives under content/ or app/.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Pagefind previously indexed all 89 built pages including marketing pages
(home, cloud, news, etc.). Restrict indexing to /docs/* via a route-based
--glob pattern so newly added marketing pages aren't silently swept into
search results without needing per-page annotations.

Also fixes the /api/md route leaking a raw JSX comment ({/* ... */}) from
content/docs/deploy/aws-cdk.mdx into the crawler-facing Markdown output,
since aws-cdk.mdx's content was recently inlined with a maintainer note
written as a JSX comment.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
… fallback, /docs.md route, dead redirects cleanup

- Fix broken /docs/.md alternate link emitted on the /docs landing page
  (only emit for actual docs pages with mdxPath.length > 1)
- Harden the markdown route against path traversal by rejecting resolved
  paths outside content/docs
- Fall back to .md source files when .mdx doesn't exist (fixes 3 docs
  pages that 404 on their markdown endpoint)
- Add /docs.md rewrite and serve content/docs/index.mdx for the index
  route (route folder converted to optional catch-all)
- Fix dead /docs/web-apps(/index) redirect (was keyed with a hash that
  never reaches the server) and add /docs/web-apps redirect
- Filter hash-anchor entries out of next.config redirects() since the
  server never sees the hash; they remain in redirects.js for the
  client-side HashRedirects component

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
- D1: add explicit leading-tight to hero <h1>s (home + cloud) so the
  multi-line headings no longer collapse/overlap under the theme's
  competing line-height rules.
- D2: add @source '../content/docs' to globals.css so Tailwind v4 scans
  the gitignored docs content dir again, restoring doc-only utilities
  like the maturity-matrix bg-green/yellow/red-400 dots.
- D3: set copyPage: false on all full-layout marketing pages (home,
  cloud + its /cloud/* fragment routes, support, sentry, xray) so the
  docs "Copy page" button no longer leaks onto marketing pages.
- D4: give credits the same full-layout theme block as other marketing
  pages so it renders without docs sidebar/TOC/copy-page/feedback chrome.

D5 (news dashboard image) verified as a screenshot lazy-load timing
artifact, not a real bug - the image loads correctly given time.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
…ecision

Maintainer accepted these two v2 look-and-feel restorations while leaving the
rest of the v4 defaults (content width, callout colors, external-link arrows,
sidebar active style) as-is:
- Search placeholder: "Search docs" instead of v4's default "Search documentation…"
- Code blocks: restore the light-blue background tint instead of v4's neutral one

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
…ne-height

The previous leading-tight fix was a no-op. The actual bug: because the raw
JSX <h1> in content/index.mdx (an .mdx file) had its text content split
across multiple source lines, MDX's remark processing wrapped it in a
<p class="x:leading-7"> child, whose 28px line-height overrode the h1's own
leading and collapsed the two-line heading onto itself.

Fix: put the h1's full text content on a single source line so MDX treats it
as inline phrasing content instead of a block-level paragraph, avoiding the
injected <p> entirely.

Measured before claiming success: h1 now has no child <p>, and its bounding
box height is exactly N x its computed line-height on both desktop (120px =
2 x 60px) and mobile (135px = 3 x 45px). The Bref Cloud hero (BrefCloud.jsx)
was checked too but never had this bug — it's a plain .jsx component, not
MDX, so its multi-line JSX text was never paragraph-wrapped.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Pages whose _meta.js sets theme.layout: 'full' (home, cloud, support,
credits, sentry, xray) must render edge-to-edge like on production. In
Nextra v4, layout: 'full' only drops the TOC reservation; the theme's MDX
wrapper still constrains content to --nextra-content-width (90rem).

Resolve the active layout the same way the theme does (normalizePages) in
the catch-all route and tag full pages with data-full-layout. globals.css
then overrides the documented --nextra-content-width variable on that
container (100% = viewport-wide) and removes the article's horizontal
padding. Navbar/footer keep their 90rem inner width since they render
outside this container.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
- hero-bg.jsx: SVG stop-color -> stopColor (JSX camelCase, invalid DOM prop)
- features.jsx: add key={tier.id} to tiers.map() list children

Swept /, /cloud, /docs, a docs page, /support, /credits with playwright-cli;
no other React key/prop/DOM warnings found.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Nextra's <Head> injects a :root block for --nextra-bg and --nextra-primary-*
that overrides globals.css, so these must be set via Head props, not CSS:

- backgroundColor={{ light: '#fff', dark: '#111' }}: page background is pure
  white instead of Nextra's default grey rgb(250,250,250). Also paint <body>
  white via the same --nextra-bg var (Head only paints <html>).
- color={{ hue: 202, saturation: 80, lightness: 57 }}: primary is Bref
  blue-500 hsl(202,80%,57%) = #3AA9E9, not Nextra's default hue-212 blue. Docs
  links and the active sidebar item now use the Bref palette. Removed the dead
  --nextra-primary-* :root override in globals.css (Head's block won anyway).

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Nextra v4 no longer collapses a `X.mdx` page next to a sibling `X/` folder
into one link (as v2 did): it renders an expandable folder with the page
duplicated as a child (Setup > Setup), or a dead button header for folders
that do have children (Deployment > Deployment, Docker, CDK).

Fix the Nextra v4 way: move each `X.mdx` to `X/index.mdx` with
`asIndexPage: true` frontmatter. normalize.js then absorbs the index page
into the folder node (folder route == index route), so the folder header is
the page link with no duplicate child; folders whose only other children are
`display: hidden` (setup, monitoring) render as a single flat link like prod.

Applied to all 7 page+folder pairs with nested pages: setup, deploy,
runtimes, case-studies, local-development, monitoring, use-cases/http.
Routes are unchanged (/docs/X), so markdown links and inbound links stay as
-is; only webpack imports (component + image ES imports) get the extra ../.

content/docs is a build-time copy (gitignored); sync via `make`/sync-directory.

Claude-Session: https://claude.ai/code/session_01Q293dCZ4iL2Qys9rcSGwcR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant