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.jsis 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.ts → postPublish 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 siteThis 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) →
Productschema (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) →
Recipeschema: ingredients and instructions map almost directly ontorecipeIngredientandrecipeInstructions, since we’re already modeling them as structured array fields rather than free-text. - Every page →
Organization/WebSiteschema at the layout level (brand name, logo, social profiles), andBreadcrumbListfor 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.