# Web Manager — Public Site (Next.js) How the public site renders what [Web Manager](./Readme.md) publishes. Source: `apps/web/web-runtime/`, `apps/web/app/`. ## Structure ```txt app/[locale]/[[...path]]/page.tsx localized catch-all app/[locale]/preview/page.tsx token-gated draft rendering, noindex, force-dynamic app/api/revalidate/route.ts secret-guarded tag purge app/sitemap.ts, app/robots.ts generated from the public API web-runtime/api/ HTTP client, render-data resolution, cache tags web-runtime/data/ transport normalization web-runtime/blocks/ block renderers + registry web-runtime/layouts/ page layouts and hero web-runtime/chrome/ header and footer web-runtime/components/ link, rich text and media primitives web-runtime/lib/ metadata, site URL, sitemap, structured data web-runtime/types.ts normalized public contract web-runtime/renderer.tsx header + layout + footer ``` Locales come from `site.config.ts` (`en`, `uk`, `de`, default `en`); with more than one locale next-intl always prefixes the URL. Everything degrades rather than fails. An unknown `page_type` falls back to the standard layout; an unknown block type renders a dev-only "renderer missing" banner and nothing at all in production; the normalizer degrades malformed payloads instead of throwing. Publishing a block type the web app does not know yet can therefore never take the site down. ## Hero Hero impact is an intensity choice, not a block type: | Impact | Use for | Rendering | | -------- | ------------------------------- | ------------------------------------------------------- | | `none` | pages that start with content | hero omitted | | `low` | legal, utility, index pages | compact padding, smallest title scale | | `middle` | default marketing and content | medium padding, balanced scale | | `high` | campaigns, launches, conversion | near-viewport height, largest scale, radial accent wash | Media beside the hero switches it to a two-column grid at every impact. A page with no hero object falls back to a page-type eyebrow plus the title. ## SEO `generateMetadata` delegates to `pageMetadata` (`web-runtime/lib/page-metadata.ts`), a pure function of the resolved render data: - **title / description** — the SEO fields, falling back to the page title; an empty description is omitted rather than sent blank. - **canonical** — the page's own locale-prefixed path. - **hreflang** — one alternate per locale in `languageAlternatives`, plus `x-default` pointing at the default locale, or at this page when the default locale has no version. Omitted for a page with no translations. - **Open Graph / Twitter** — the hero image becomes the share card with width, height and alt when supplied; `summary_large_image` with an image, `summary` without; `og:type` is `article` for `blog-item`, `website` otherwise. `app/sitemap.ts` walks the paginated sitemap endpoint (capped at 50 requests — the protocol's 50,000-URL limit) and hands entries to `buildSitemap`; pages sharing a `groupKey` link to each other through `alternates.languages`, and `lastModified` uses `updatedAt` falling back to `publishedAt`. `app/robots.ts` allows everything except `/*/preview` and advertises the sitemap. `web-runtime/lib/structured-data.tsx` emits `BreadcrumbList` on every page with a trail and `Article` on editorial pages — skipped entirely without a publication date rather than shipping markup a crawler discards. **`SITE_URL` must be set.** `metadataBase` comes from `siteUrl()`, and Next resolves every relative metadata URL against it. Without it Next falls back to `http://localhost:3000`: the build succeeds and pages look fine, but canonicals and share cards point at localhost. A production build without it logs an error; a malformed value throws.