Skip to Content
Sitemap, Structured Data & Redirects

Sitemap, Structured Data & Redirects

Pulled out as its own page deliberately: of everything under SEO & GEO, these are the pieces most likely to actually block a production launch or hold back real SEO/GEO results, and the ones worth tracking on their own rather than as bullets buried inside a longer page. SEO itself, by contrast, is genuinely native and solid; this page is specifically what it doesn’t cover.

sitemap.xml and robots.txt

Both are standard Next.js App Router conventions, not P1-specific work: app/sitemap.ts and app/robots.ts, each exporting a function Next.js recognizes automatically. Confirmed absent from the scaffold: no app/sitemap.ts, no app/robots.ts, no SDK-level generator for either.

The P1-specific part is the data source: the sitemap needs the full list of published document paths, which is exactly what documents.list(siteId, branchId) (see Workflows) already provides. A brand site’s app/sitemap.ts calls that, filters out _registry/internal paths the same way the catch-all route already does (isInternalPath in app/[...puckPath]/page.tsx), and maps the result to sitemap entries.

The sitemap must update itself: no rebuild, ever

This is a real requirement, not a nice-to-have: when a site manager adds or removes a page through the P1 editor, the sitemap has to reflect that without anyone rebuilding or redeploying the Next.js app. Confirmed directly from Next.js’s own docs (v16.3.3, matching this project):

sitemap.js is a special Route Handler that is cached by default unless it uses a Request-time API or dynamic config option.

“Cached by default” is the trap. A naive app/sitemap.ts that calls documents.list() will still get cached and served stale until something tells Next.js to regenerate it. A full rebuild would work, but so does the exact mechanism the scaffold already uses everywhere else for this problem, and there’s no reason to reach for anything heavier:

// app/sitemap.ts import type { MetadataRoute } from "next"; // Same backstop pattern as app/[...puckPath]/page.tsx's own `revalidate` export; // bounds staleness even if the explicit revalidation below is ever missed. export const revalidate = 300; export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const docs = await client.documents.list(siteId, branchId); return docs .filter((d) => !isInternalPath(d.path)) .map((d) => ({ url: `${baseUrl}${d.path}`, lastModified: d.updatedAt })); }

That alone gives you sitemap freshness within 5 minutes, automatically, forever, with no dev team involvement. For immediate freshness instead of “within 5 minutes,” extend the existing publish/create/delete flow: the scaffold’s own app/p1/api/[...p1]/route.tspostPublish handler already calls revalidatePath(path) for the specific page whenever something is published (see p1-next-sdk’s routes/publish.js). Add one more call there:

revalidatePath(path); // already there, busts the edited page's cache revalidatePath("/sitemap.xml"); // add this, busts the sitemap too, same call site

This is the same mechanism already proven elsewhere in this codebase (revalidate as a time-based backstop, revalidatePath as an immediate trigger on publish), not new infrastructure, not a build step, and specifically not something that requires “compiling.” The sitemap becomes just another cached route that P1’s existing publish hook already knows how to bust.

Worth building once as a shared utility rather than per-brand: a buildSitemapFromDocuments() helper alongside the other Brown-Forman-shared packages, since every brand site needs the identical logic against the identical API.

Redirects

The platform supports redirects; this scaffold just doesn’t use that support yet. Confirmed directly from @pantheon-systems/css-client’s own types: a real RedirectInfo shape and a getRedirect(path) method exist on P1ContentClient. But grep-ing this project’s app/ and lib/ directories for getRedirect or RedirectInfo turns up nothing: nowhere in the rendering pipeline is it ever called.

What the SDK actually exposes:

interface RedirectInfo { fromPath: string; destination: string; redirectType: "permanent" | "temporary"; parenting: boolean; // whether child paths under fromPath also redirect statusCode: 301 | 302 | 303 | 307 | 308; } getRedirect(path: string): Promise<RedirectInfo | null>;

This means: if a page’s URL changes and something on the Pantheon side is configured to redirect the old path, that redirect will not currently fire on this site: the catch-all route (app/[...puckPath]/page.tsx) has no code path that calls getRedirect() before falling through to a 404. Whether redirects are actually configurable today is unconfirmed in a specific way worth being precise about: getRedirect() lives in @pantheon-systems/p1-next-sdk, not @pantheon-systems/css-client at all, and css-client’s own endpoint list (the SDK the content dashboard is built on) has no redirects endpoint of any kind, read or write. We found the one read-side lookup method; we did not find where or how a redirect actually gets created anywhere in either package.

To close this gap: the catch-all route needs a getRedirect(path) check ahead of (or alongside) its normal document lookup, issuing a real HTTP redirect at the matched statusCode when one exists. Straightforward to add: the missing piece is wiring, not a platform capability.

Structured data (JSON-LD)

Confirmed absent by direct search: zero occurrences of application/ld+json anywhere in the scaffolded app or the installed @pantheon-systems/* SDK packages. No Product, Recipe, Organization, or BreadcrumbList schema. This matters for classic SEO (rich results: star ratings, recipe cards, product pricing in search) and matters more for GEO: structured data is one of the few genuinely reliable signals AI answer engines use to extract facts confidently rather than guessing from prose. See GEO.

No SDK-level support exists to build on, so this is genuinely new work, best scoped per-Template rather than site-wide:

  • Products (Products Template) → Product schema (name, image, description; offers/price data only if we actually have pricing, which the retail-locator integration suggests we may not; Dotter owns pricing/availability, not us).
  • Cocktails (Cocktails Template) → Recipe schema: ingredients and instructions map almost directly onto recipeIngredient and recipeInstructions, since we’re already modeling them as structured array fields rather than free-text.
  • Every pageOrganization/WebSite schema at the layout level (brand name, logo, social profiles), and BreadcrumbList for any page with a real hierarchy.

Each Template’s render function gets a co-located function that emits the matching <script type="application/ld+json"> block from that template’s own fields; the data’s already structured (that’s what a Puck field is), so this is a mapping exercise, not new data modeling.

Open questions for this page now live on Outstanding Questions, tracked centrally across all pages rather than repeated per page.

Last updated on