diff --git a/server/handlers/cms/data/preview.ts b/server/handlers/cms/data/preview.ts index 7e2821b4..57644952 100644 --- a/server/handlers/cms/data/preview.ts +++ b/server/handlers/cms/data/preview.ts @@ -94,6 +94,9 @@ export async function handleRowPreview( return jsonResponse({ error: 'No entry template found for this collection' }, { status: 404 }) } const merged = composeTemplateChain(chain, { kind: 'entry' }) + // The template chain has no Page for the entry, so composeTemplateChain + // can't know its title — the entry's own (draft) title is the real document title. + if (typeof draftCells.title === 'string') merged.title = draftCells.title // Build a synthetic PublishedDataRow with the draft cells merged in. // Bindings inside the template (`{currentEntry.body}`, featured-media diff --git a/server/publish/publicRenderer.ts b/server/publish/publicRenderer.ts index 19792a7b..a35e0177 100644 --- a/server/publish/publicRenderer.ts +++ b/server/publish/publicRenderer.ts @@ -176,6 +176,9 @@ export async function renderPublishedDataRowTemplate( const chain = resolveTemplateChain(snapshot.site, { kind: 'entry', tableSlug: row.tableSlug }) if (chain.length === 0) return null // no entry template → 404 (unchanged behaviour) const merged = composeTemplateChain(chain, { kind: 'entry' }) + // The template chain has no Page for the entry, so composeTemplateChain + // can't know its title — the entry's own title is the real document title. + if (typeof row.cells.title === 'string') merged.title = row.cells.title // Seed the entry stack with the published row + route frame from the request // URL. Loop interceptors push/pop iteration items on top of this stack; diff --git a/src/core/templates/templateCompose.ts b/src/core/templates/templateCompose.ts index 95bda45a..628c1f0e 100644 --- a/src/core/templates/templateCompose.ts +++ b/src/core/templates/templateCompose.ts @@ -140,7 +140,11 @@ export function composeTemplateChain(chain: Page[], terminal: TerminalContent): return { id: innermost.id, // identifies "what was rendered" for the publish.html filter slug: innermost.slug, - title: innermost.title, + // The terminal page's own title is the document's real title — the + // wrapping template's title is chrome, never `` content. Entry + // terminals have no Page here; their caller overrides `title` from the + // published row after composing. + title: terminal.kind === 'page' ? terminal.page.title : innermost.title, rootNodeId: acc.rootId, nodes: acc.nodes, }