diff --git a/.changeset/yellow-olives-sing.md b/.changeset/yellow-olives-sing.md new file mode 100644 index 000000000000..8ba00915e5e5 --- /dev/null +++ b/.changeset/yellow-olives-sing.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes head propagation regression diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index fb34954ac743..635ef033132b 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -2033,6 +2033,8 @@ export interface SSRMetadata { headInTree: boolean; extraHead: string[]; propagators: Map; + // Used to key track of unique content; links and script tags + contentKeys: Set; } /* Preview server stuff */ diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index a1afb2201226..112428587353 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -6,7 +6,7 @@ import { createComponent, createHeadAndContent, renderComponent, - renderScriptElement, + renderUniqueScriptElement, renderTemplate, renderUniqueStylesheet, unescapeHTML, @@ -303,7 +303,7 @@ async function render({ .join(''); } if (Array.isArray(collectedScripts)) { - scripts = collectedScripts.map((script: any) => renderScriptElement(script)).join(''); + scripts = collectedScripts.map((script: any) => renderUniqueScriptElement(result, script)).join(''); } let props = baseProps; diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 968b232d48f0..2bfdb99b01f9 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -258,6 +258,7 @@ export function createResult(args: CreateResultArgs): SSRResult { headInTree: false, extraHead: [], propagators: new Map(), + contentKeys: new Set(), }, }; diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index aca260d009de..9acc412328b6 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -28,6 +28,7 @@ export { renderTemplate, renderToString, renderUniqueStylesheet, + renderUniqueScriptElement, voidElementNames, } from './render/index.js'; export type { diff --git a/packages/astro/src/runtime/server/render/astro/render.ts b/packages/astro/src/runtime/server/render/astro/render.ts index 89dc28b759a5..0cb7d43fa876 100644 --- a/packages/astro/src/runtime/server/render/astro/render.ts +++ b/packages/astro/src/runtime/server/render/astro/render.ts @@ -71,6 +71,10 @@ export async function renderToReadableStream( // If the Astro component returns a Response on init, return that response if (templateResult instanceof Response) return templateResult; + if (isPage) { + await bufferHeadContent(result); + } + let renderedFirstPageChunk = false; return new ReadableStream({ @@ -144,3 +148,19 @@ async function callComponentAsTemplateResultOrResponse( return isHeadAndContent(factoryResult) ? factoryResult.content : factoryResult; } + +// Recursively calls component instances that might have head content +// to be propagated up. +async function bufferHeadContent(result: SSRResult) { + const iterator = result._metadata.propagators.values(); + while (true) { + const { value, done } = iterator.next(); + if (done) { + break; + } + const returnValue = await value.init(result); + if (isHeadAndContent(returnValue)) { + result._metadata.extraHead.push(returnValue.head); + } + } +} diff --git a/packages/astro/src/runtime/server/render/index.ts b/packages/astro/src/runtime/server/render/index.ts index 70d63ca600eb..4760e07b7c60 100644 --- a/packages/astro/src/runtime/server/render/index.ts +++ b/packages/astro/src/runtime/server/render/index.ts @@ -6,6 +6,6 @@ export { renderHTMLElement } from './dom.js'; export { maybeRenderHead, renderHead } from './head.js'; export { renderPage } from './page.js'; export { renderSlot, renderSlotToString, type ComponentSlots } from './slot.js'; -export { renderScriptElement, renderUniqueStylesheet } from './tags.js'; +export { renderScriptElement, renderUniqueScriptElement, renderUniqueStylesheet } from './tags.js'; export type { RenderInstruction } from './types'; export { addAttribute, defineScriptVars, voidElementNames } from './util.js'; diff --git a/packages/astro/src/runtime/server/render/tags.ts b/packages/astro/src/runtime/server/render/tags.ts index f15da65716bd..dd38f27be5c7 100644 --- a/packages/astro/src/runtime/server/render/tags.ts +++ b/packages/astro/src/runtime/server/render/tags.ts @@ -9,14 +9,43 @@ export function renderScriptElement({ props, children }: SSRElement) { }); } +export function renderUniqueScriptElement(result: SSRResult, { props, children }: SSRElement) { + if(Array.from(result.scripts).some(s => { + if(s.props.type === props.type && s.props.src === props.src) { + return true; + } + if(!props.src && s.children === children) return true; + })) return ''; + const key = `script-${props.type}-${props.src}-${children}`; + if(checkOrAddContentKey(result, key)) return ''; + return renderScriptElement({ props, children }); + +} + export function renderUniqueStylesheet(result: SSRResult, sheet: StylesheetAsset) { if (sheet.type === 'external') { if (Array.from(result.styles).some((s) => s.props.href === sheet.src)) return ''; - return renderElement('link', { props: { rel: 'stylesheet', href: sheet.src }, children: '' }); + const key = 'link-external-' + sheet.src; + if(checkOrAddContentKey(result, key)) return ''; + return renderElement('link', { + props: { + rel: 'stylesheet', + href: sheet.src + }, + children: '' + }); } if (sheet.type === 'inline') { if (Array.from(result.styles).some((s) => s.children.includes(sheet.content))) return ''; + const key = `link-inline-` + sheet.content; + if(checkOrAddContentKey(result, key)) return ''; return renderElement('style', { props: { type: 'text/css' }, children: sheet.content }); } } + +function checkOrAddContentKey(result: SSRResult, key: string): boolean { + if(result._metadata.contentKeys.has(key)) return true; + result._metadata.contentKeys.add(key); + return false; +}