diff --git a/.changeset/pretty-bananas-own.md b/.changeset/pretty-bananas-own.md
new file mode 100644
index 000000000000..322ce96e24c8
--- /dev/null
+++ b/.changeset/pretty-bananas-own.md
@@ -0,0 +1,10 @@
+---
+'astro': patch
+---
+
+Fix MDX related head placement bugs
+
+This fixes a variety of head content placement bugs (such as page ``) related to MDX, especially when used in content collections. Issues fixed:
+
+- Head content being placed in the body instead of the head.
+- Head content missing when rendering an MDX component from within a nested Astro component.
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 9b189a73bd86..35b83d5a9341 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -99,7 +99,7 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
- "@astrojs/compiler": "^1.0.1",
+ "@astrojs/compiler": "^1.1.0",
"@astrojs/language-server": "^0.28.3",
"@astrojs/markdown-remark": "^2.0.1",
"@astrojs/telemetry": "^2.0.0",
diff --git a/packages/astro/src/content/internal.ts b/packages/astro/src/content/internal.ts
index 04280166feb3..ebfb971be44a 100644
--- a/packages/astro/src/content/internal.ts
+++ b/packages/astro/src/content/internal.ts
@@ -4,6 +4,7 @@ import { prependForwardSlash } from '../core/path.js';
import {
createComponent,
createHeadAndContent,
+ createScopedResult,
renderComponent,
renderScriptElement,
renderStyleElement,
@@ -169,7 +170,7 @@ async function render({
return createHeadAndContent(
unescapeHTML(styles + links + scripts) as any,
- renderTemplate`${renderComponent(result, 'Content', mod.Content, props, slots)}`
+ renderTemplate`${renderComponent(createScopedResult(result), 'Content', mod.Content, props, slots)}`
);
},
propagation: 'self',
diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts
index 2da98f1c6022..4b14321762af 100644
--- a/packages/astro/src/core/app/common.ts
+++ b/packages/astro/src/core/app/common.ts
@@ -14,10 +14,12 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest):
}
const assets = new Set(serializedManifest.assets);
+ const propagation = new Map(serializedManifest.propagation);
return {
...serializedManifest,
assets,
+ propagation,
routes,
};
}
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index 529a450f9531..615be50aa585 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -193,6 +193,7 @@ export class App {
request,
origin: url.origin,
pathname,
+ propagation: this.#manifest.propagation,
scripts,
links,
route: routeData,
diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts
index 587c924b92de..cf85d0387bce 100644
--- a/packages/astro/src/core/app/types.ts
+++ b/packages/astro/src/core/app/types.ts
@@ -1,9 +1,11 @@
import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark';
import type {
ComponentInstance,
+ PropagationHint,
RouteData,
SerializedRouteData,
SSRLoadedRenderer,
+ SSRResult,
} from '../../@types/astro';
export type ComponentPath = string;
@@ -34,11 +36,13 @@ export interface SSRManifest {
renderers: SSRLoadedRenderer[];
entryModules: Record;
assets: Set;
+ propagation: SSRResult['propagation'];
}
-export type SerializedSSRManifest = Omit & {
+export type SerializedSSRManifest = Omit & {
routes: SerializedRouteInfo[];
assets: string[];
+ propagation: readonly [string, PropagationHint][];
};
export type AdapterCreateExports = (
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index 1a708aab3765..f5f72dc5ab93 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -376,6 +376,7 @@ async function generatePath(
origin,
pathname,
request: createRequest({ url, headers: new Headers(), logging, ssr }),
+ propagation: internals.propagation,
scripts,
links,
route: pageData.route,
diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts
index dc660f4949e0..87c817ad4bca 100644
--- a/packages/astro/src/core/build/internal.ts
+++ b/packages/astro/src/core/build/internal.ts
@@ -4,6 +4,7 @@ import type { PageBuildData, ViteID } from './types';
import { PageOptions } from '../../vite-plugin-astro/types';
import { prependForwardSlash, removeFileExtension } from '../path.js';
import { viteID } from '../util.js';
+import { SSRResult } from '../../@types/astro';
export interface BuildInternals {
/**
@@ -66,6 +67,7 @@ export interface BuildInternals {
staticFiles: Set;
// The SSR entry chunk. Kept in internals to share between ssr/client build steps
ssrEntryChunk?: OutputChunk;
+ propagation: SSRResult['propagation'];
}
/**
@@ -95,6 +97,7 @@ export function createBuildInternals(): BuildInternals {
discoveredClientOnlyComponents: new Set(),
discoveredScripts: new Set(),
staticFiles: new Set(),
+ propagation: new Map(),
};
}
diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts
index cf0f9bf7c66d..9aa3e20b5801 100644
--- a/packages/astro/src/core/build/plugins/index.ts
+++ b/packages/astro/src/core/build/plugins/index.ts
@@ -8,6 +8,7 @@ import { pluginInternals } from './plugin-internals.js';
import { pluginPages } from './plugin-pages.js';
import { pluginPrerender } from './plugin-prerender.js';
import { pluginSSR } from './plugin-ssr.js';
+import { astroHeadPropagationBuildPlugin } from '../../../vite-plugin-head-propagation/index.js';
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
register(pluginAliasResolve(internals));
@@ -15,6 +16,7 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP
register(pluginInternals(internals));
register(pluginPages(options, internals));
register(pluginCSS(options, internals));
+ register(astroHeadPropagationBuildPlugin(options, internals));
register(pluginPrerender(options, internals));
register(astroConfigBuildPlugin(options, internals));
register(pluginHoistedScripts(options, internals));
diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts
index cfc58d71b345..d4536b92c8ab 100644
--- a/packages/astro/src/core/build/plugins/plugin-ssr.ts
+++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts
@@ -211,6 +211,7 @@ function buildManifest(
contentDir: getContentPaths(settings.config).contentDir,
},
pageMap: null as any,
+ propagation: Array.from(internals.propagation),
renderers: [],
entryModules,
assets: staticFiles.map((s) => settings.config.base + s),
diff --git a/packages/astro/src/core/compile/compile.ts b/packages/astro/src/core/compile/compile.ts
index 7e8dc8a31927..a0199a2fb703 100644
--- a/packages/astro/src/core/compile/compile.ts
+++ b/packages/astro/src/core/compile/compile.ts
@@ -42,6 +42,7 @@ export async function compile({
sourcemap: 'both',
internalURL: 'astro/server/index.js',
astroGlobalArgs: JSON.stringify(astroConfig.site),
+ resultScopedSlot: true,
preprocessStyle: createStylePreprocessor({
filename,
viteConfig,
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index 3a6b0bf964d6..e236b35a4bc5 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -9,7 +9,7 @@ import type {
SSRLoadedRenderer,
SSRResult,
} from '../../@types/astro';
-import { renderSlot, stringifyChunk } from '../../runtime/server/index.js';
+import { renderSlot, stringifyChunk, ScopeFlags, createScopedResult, ComponentSlots } from '../../runtime/server/index.js';
import { renderJSX } from '../../runtime/server/jsx.js';
import { AstroCookies } from '../cookies/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
@@ -55,10 +55,10 @@ function getFunctionExpression(slot: any) {
class Slots {
#result: SSRResult;
- #slots: Record | null;
+ #slots: ComponentSlots | null;
#loggingOpts: LogOptions;
- constructor(result: SSRResult, slots: Record | null, logging: LogOptions) {
+ constructor(result: SSRResult, slots: ComponentSlots | null, logging: LogOptions) {
this.#result = result;
this.#slots = slots;
this.#loggingOpts = logging;
@@ -89,6 +89,7 @@ class Slots {
public async render(name: string, args: any[] = []) {
if (!this.#slots || !this.has(name)) return;
+ const scoped = createScopedResult(this.#result, ScopeFlags.RenderSlot);
if (!Array.isArray(args)) {
warn(
this.#loggingOpts,
@@ -97,26 +98,26 @@ class Slots {
);
} else if (args.length > 0) {
const slotValue = this.#slots[name];
- const component = typeof slotValue === 'function' ? await slotValue() : await slotValue;
+ const component = typeof slotValue === 'function' ? await slotValue(scoped) : await slotValue;
// Astro
const expression = getFunctionExpression(component);
if (expression) {
- const slot = expression(...args);
- return await renderSlot(this.#result, slot).then((res) =>
+ const slot = () => expression(...args);
+ return await renderSlot(scoped, slot).then((res) =>
res != null ? String(res) : res
);
}
// JSX
if (typeof component === 'function') {
- return await renderJSX(this.#result, component(...args)).then((res) =>
+ return await renderJSX(scoped, (component as any)(...args)).then((res) =>
res != null ? String(res) : res
);
}
}
- const content = await renderSlot(this.#result, this.#slots[name]);
- const outHTML = stringifyChunk(this.#result, content);
+ const content = await renderSlot(scoped, this.#slots[name]);
+ const outHTML = stringifyChunk(scoped, content);
return outHTML;
}
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index ccdfd5e1968f..aa9da8696b00 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -5,10 +5,13 @@ export { escapeHTML, HTMLBytes, HTMLString, markHTMLString, unescapeHTML } from
export { renderJSX } from './jsx.js';
export {
addAttribute,
+ addScopeFlag,
createHeadAndContent,
+ createScopedResult,
defineScriptVars,
Fragment,
maybeRenderHead,
+ removeScopeFlag,
renderAstroTemplateResult as renderAstroComponent,
renderComponent,
renderComponentToIterable,
@@ -23,15 +26,15 @@ export {
renderTemplate,
renderToString,
renderUniqueStylesheet,
+ ScopeFlags,
stringifyChunk,
voidElementNames,
} from './render/index.js';
export type {
AstroComponentFactory,
AstroComponentInstance,
- AstroComponentSlots,
- AstroComponentSlotsWithValues,
RenderInstruction,
+ ComponentSlots
} from './render/index.js';
import { markHTMLString } from './escape.js';
diff --git a/packages/astro/src/runtime/server/jsx.ts b/packages/astro/src/runtime/server/jsx.ts
index 8b81a641ea35..665e4afec8fb 100644
--- a/packages/astro/src/runtime/server/jsx.ts
+++ b/packages/astro/src/runtime/server/jsx.ts
@@ -12,7 +12,7 @@ import {
} from './index.js';
import { HTMLParts } from './render/common.js';
import type { ComponentIterable } from './render/component';
-import { ScopeFlags } from './render/util.js';
+import { createScopedResult, ScopeFlags } from './render/scope.js';
const ClientOnlyPlaceholder = 'astro-client-only';
@@ -95,8 +95,9 @@ Did you forget to import the component or is it possible there is a typo?`);
props[key] = value;
}
}
- result.scope |= ScopeFlags.JSX;
- return markHTMLString(await renderToString(result, vnode.type as any, props, slots));
+ const scoped = createScopedResult(result, ScopeFlags.JSX);
+ const html = markHTMLString(await renderToString(scoped, vnode.type as any, props, slots));
+ return html;
}
case !vnode.type && (vnode.type as any) !== 0:
return '';
diff --git a/packages/astro/src/runtime/server/render/astro/factory.ts b/packages/astro/src/runtime/server/render/astro/factory.ts
index 1f85fe45c72f..7f5b912472ef 100644
--- a/packages/astro/src/runtime/server/render/astro/factory.ts
+++ b/packages/astro/src/runtime/server/render/astro/factory.ts
@@ -3,7 +3,7 @@ import type { HeadAndContent } from './head-and-content';
import type { RenderTemplateResult } from './render-template';
import { HTMLParts } from '../common.js';
-import { ScopeFlags } from '../util.js';
+import { addScopeFlag, createScopedResult, ScopeFlags } from '../scope.js';
import { isHeadAndContent } from './head-and-content.js';
import { renderAstroTemplateResult } from './render-template.js';
@@ -28,8 +28,8 @@ export async function renderToString(
props: any,
children: any
): Promise {
- result.scope |= ScopeFlags.Astro;
- const factoryResult = await componentFactory(result, props, children);
+ const scoped = createScopedResult(result, ScopeFlags.Astro);
+ const factoryResult = await componentFactory(scoped, props, children);
if (factoryResult instanceof Response) {
const response = factoryResult;
diff --git a/packages/astro/src/runtime/server/render/astro/index.ts b/packages/astro/src/runtime/server/render/astro/index.ts
index 03d97b14875c..cbddf7876e8e 100644
--- a/packages/astro/src/runtime/server/render/astro/index.ts
+++ b/packages/astro/src/runtime/server/render/astro/index.ts
@@ -1,7 +1,7 @@
export type { AstroComponentFactory } from './factory';
export { isAstroComponentFactory, renderToString } from './factory.js';
export { createHeadAndContent, isHeadAndContent } from './head-and-content.js';
-export type { AstroComponentInstance, ComponentSlots, ComponentSlotsWithValues } from './instance';
+export type { AstroComponentInstance } from './instance';
export { createAstroComponentInstance, isAstroComponentInstance } from './instance.js';
export {
isRenderTemplateResult,
diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts
index d290abcc1640..171cc1a546de 100644
--- a/packages/astro/src/runtime/server/render/astro/instance.ts
+++ b/packages/astro/src/runtime/server/render/astro/instance.ts
@@ -1,17 +1,15 @@
import type { SSRResult } from '../../../../@types/astro';
import type { AstroComponentFactory, AstroFactoryReturnValue } from './factory.js';
-import type { renderTemplate } from './render-template.js';
+import type { ComponentSlots } from '../slot.js';
import { HydrationDirectiveProps } from '../../hydration.js';
import { isPromise } from '../../util.js';
import { renderChild } from '../any.js';
import { isAPropagatingComponent } from './factory.js';
import { isHeadAndContent } from './head-and-content.js';
+import { createScopedResult, ScopeFlags } from '../scope.js';
type ComponentProps = Record;
-type ComponentSlotValue = () => ReturnType;
-export type ComponentSlots = Record;
-export type ComponentSlotsWithValues = Record>;
const astroComponentInstanceSym = Symbol.for('astro.componentInstance');
@@ -20,7 +18,7 @@ export class AstroComponentInstance {
private readonly result: SSRResult;
private readonly props: ComponentProps;
- private readonly slotValues: ComponentSlotsWithValues;
+ private readonly slotValues: ComponentSlots;
private readonly factory: AstroComponentFactory;
private returnValue: ReturnType | undefined;
constructor(
@@ -33,19 +31,21 @@ export class AstroComponentInstance {
this.props = props;
this.factory = factory;
this.slotValues = {};
+ const scoped = createScopedResult(result, ScopeFlags.Slot);
for (const name in slots) {
- this.slotValues[name] = slots[name]();
+ const value = slots[name](scoped);
+ this.slotValues[name] = () => value;
}
}
- async init() {
- this.returnValue = this.factory(this.result, this.props, this.slotValues);
+ async init(result: SSRResult) {
+ this.returnValue = this.factory(result, this.props, this.slotValues);
return this.returnValue;
}
async *render() {
if (this.returnValue === undefined) {
- await this.init();
+ await this.init(this.result);
}
let value: AstroFactoryReturnValue | undefined = this.returnValue;
diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts
index def7dedd37e0..5283eaa12b14 100644
--- a/packages/astro/src/runtime/server/render/common.ts
+++ b/packages/astro/src/runtime/server/render/common.ts
@@ -10,6 +10,7 @@ import {
} from '../scripts.js';
import { renderAllHeadContent } from './head.js';
import { isSlotString, type SlotString } from './slot.js';
+import { ScopeFlags } from './scope.js';
export const Fragment = Symbol.for('astro:fragment');
export const Renderer = Symbol.for('astro:renderer');
@@ -48,6 +49,30 @@ export function stringifyChunk(result: SSRResult, chunk: string | SlotString | R
}
return renderAllHeadContent(result);
}
+ case 'maybe-head': {
+ if (result._metadata.hasRenderedHead) {
+ return '';
+ }
+
+ const scope = instruction.scope;
+ switch (scope) {
+ // JSX with an Astro slot
+ case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro:
+ case ScopeFlags.JSX | ScopeFlags.Astro | ScopeFlags.HeadBuffer:
+ case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro | ScopeFlags.HeadBuffer: {
+ return '';
+ }
+
+ // Astro.slots.render('default') should never render head content.
+ case ScopeFlags.RenderSlot | ScopeFlags.Astro:
+ case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX:
+ case ScopeFlags.RenderSlot | ScopeFlags.Astro | ScopeFlags.JSX | ScopeFlags.HeadBuffer: {
+ return '';
+ }
+ }
+
+ return renderAllHeadContent(result);
+ }
}
} else {
if (isSlotString(chunk as string)) {
diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts
index 034e796677a2..36aec94cfbae 100644
--- a/packages/astro/src/runtime/server/render/component.ts
+++ b/packages/astro/src/runtime/server/render/component.ts
@@ -17,7 +17,7 @@ import {
} from './astro/index.js';
import { Fragment, Renderer, stringifyChunk } from './common.js';
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
-import { renderSlot, renderSlots } from './slot.js';
+import { ComponentSlots, renderSlot, renderSlots } from './slot.js';
import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
const rendererAliases = new Map([['solid', 'solid-js']]);
@@ -330,7 +330,7 @@ function sanitizeElementName(tag: string) {
return tag.trim().split(unsafe)[0].trim();
}
-async function renderFragmentComponent(result: SSRResult, slots: any = {}) {
+async function renderFragmentComponent(result: SSRResult, slots: ComponentSlots = {}) {
const children = await renderSlot(result, slots?.default);
if (children == null) {
return children;
diff --git a/packages/astro/src/runtime/server/render/head.ts b/packages/astro/src/runtime/server/render/head.ts
index 0f39fe2192a8..4bd072ee7918 100644
--- a/packages/astro/src/runtime/server/render/head.ts
+++ b/packages/astro/src/runtime/server/render/head.ts
@@ -1,7 +1,8 @@
import type { SSRResult } from '../../../@types/astro';
import { markHTMLString } from '../escape.js';
-import { renderElement, ScopeFlags } from './util.js';
+import { renderElement } from './util.js';
+import { ScopeFlags } from './scope.js';
// Filter out duplicate elements in our set
const uniqueElements = (item: any, index: number, all: any[]) => {
@@ -52,15 +53,7 @@ export function* maybeRenderHead(result: SSRResult) {
return;
}
- // Don't render the head inside of a JSX component that's inside of an Astro component
- // as the Astro component will be the one to render the head.
- switch (result.scope) {
- case ScopeFlags.JSX | ScopeFlags.Slot | ScopeFlags.Astro: {
- return;
- }
- }
-
// This is an instruction informing the page rendering that head might need rendering.
// This allows the page to deduplicate head injections.
- yield { type: 'head', result } as const;
+ yield { type: 'maybe-head', result, scope: result.scope } as const;
}
diff --git a/packages/astro/src/runtime/server/render/index.ts b/packages/astro/src/runtime/server/render/index.ts
index 012b421f5784..092feb86b258 100644
--- a/packages/astro/src/runtime/server/render/index.ts
+++ b/packages/astro/src/runtime/server/render/index.ts
@@ -1,8 +1,6 @@
export type {
AstroComponentFactory,
AstroComponentInstance,
- ComponentSlots as AstroComponentSlots,
- ComponentSlotsWithValues as AstroComponentSlotsWithValues,
} from './astro/index';
export {
createHeadAndContent,
@@ -15,7 +13,8 @@ export { renderComponent, renderComponentToIterable } from './component.js';
export { renderHTMLElement } from './dom.js';
export { maybeRenderHead, renderHead } from './head.js';
export { renderPage } from './page.js';
-export { renderSlot } from './slot.js';
+export { renderSlot, type ComponentSlots } from './slot.js';
+export { createScopedResult, ScopeFlags, addScopeFlag, removeScopeFlag } from './scope.js';
export { renderScriptElement, renderStyleElement, 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/page.ts b/packages/astro/src/runtime/server/render/page.ts
index b949019d6967..a9c1af77bb9c 100644
--- a/packages/astro/src/runtime/server/render/page.ts
+++ b/packages/astro/src/runtime/server/render/page.ts
@@ -15,6 +15,7 @@ import {
import { chunkToByteArray, encoder, HTMLParts } from './common.js';
import { renderComponent } from './component.js';
import { maybeRenderHead } from './head.js';
+import { addScopeFlag, createScopedResult, removeScopeFlag, ScopeFlags } from './scope.js';
const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
@@ -55,12 +56,13 @@ async function iterableToHTMLBytes(
// to be propagated up.
async function bufferHeadContent(result: SSRResult) {
const iterator = result.propagators.values();
+ const scoped = createScopedResult(result, ScopeFlags.HeadBuffer);
while (true) {
const { value, done } = iterator.next();
if (done) {
break;
}
- const returnValue = await value.init();
+ const returnValue = await value.init(scoped);
if (isHeadAndContent(returnValue)) {
result.extraHead.push(returnValue.head);
}
diff --git a/packages/astro/src/runtime/server/render/scope.ts b/packages/astro/src/runtime/server/render/scope.ts
new file mode 100644
index 000000000000..fb40d40f1675
--- /dev/null
+++ b/packages/astro/src/runtime/server/render/scope.ts
@@ -0,0 +1,32 @@
+import type { SSRResult } from '../../../@types/astro';
+
+export const ScopeFlags = {
+ Astro: 1 << 0, // 1
+ JSX: 1 << 1, // 2
+ Slot: 1 << 2, // 4
+ HeadBuffer: 1 << 3, // 8
+ RenderSlot: 1 << 4, // 16
+} as const;
+
+type ScopeFlagValues = (typeof ScopeFlags)[keyof typeof ScopeFlags];
+
+export function addScopeFlag(result: SSRResult, flag: ScopeFlagValues) {
+ result.scope |= flag;
+}
+
+export function removeScopeFlag(result: SSRResult, flag: ScopeFlagValues) {
+ result.scope &= ~flag;
+}
+
+export function createScopedResult(result: SSRResult, flag?: ScopeFlagValues): SSRResult {
+ const scopedResult = Object.create(result, {
+ scope: {
+ writable: true,
+ value: result.scope
+ }
+ });
+ if(flag != null) {
+ addScopeFlag(scopedResult, flag);
+ }
+ return scopedResult;
+}
diff --git a/packages/astro/src/runtime/server/render/slot.ts b/packages/astro/src/runtime/server/render/slot.ts
index 32d0a2dc1299..aac133e6ef6f 100644
--- a/packages/astro/src/runtime/server/render/slot.ts
+++ b/packages/astro/src/runtime/server/render/slot.ts
@@ -1,9 +1,14 @@
import type { SSRResult } from '../../../@types/astro.js';
import type { RenderInstruction } from './types.js';
+import type { renderTemplate } from './astro/render-template.js';
import { HTMLString, markHTMLString } from '../escape.js';
import { renderChild } from './any.js';
-import { ScopeFlags } from './util.js';
+import { ScopeFlags, createScopedResult } from './scope.js';
+
+type RenderTemplateResult = ReturnType;
+export type ComponentSlots = Record;
+export type ComponentSlotValue = (result: SSRResult) => RenderTemplateResult;
const slotString = Symbol.for('astro:slot-string');
@@ -23,12 +28,12 @@ export function isSlotString(str: string): str is any {
export async function renderSlot(
result: SSRResult,
- slotted: string,
- fallback?: any
+ slotted: ComponentSlotValue | RenderTemplateResult,
+ fallback?: ComponentSlotValue | RenderTemplateResult
): Promise {
if (slotted) {
- result.scope |= ScopeFlags.Slot;
- let iterator = renderChild(slotted);
+ const scoped = createScopedResult(result, ScopeFlags.Slot);
+ let iterator = renderChild(typeof slotted === 'function' ? slotted(scoped) : slotted);
let content = '';
let instructions: null | RenderInstruction[] = null;
for await (const chunk of iterator) {
@@ -41,11 +46,13 @@ export async function renderSlot(
content += chunk;
}
}
- // Remove the flag since we are now outside of the scope.
- result.scope &= ~ScopeFlags.Slot;
return markHTMLString(new SlotString(content, instructions));
}
- return fallback;
+
+ if(fallback) {
+ return renderSlot(result, fallback);
+ }
+ return '';
}
interface RenderSlotsResult {
@@ -53,13 +60,13 @@ interface RenderSlotsResult {
children: Record;
}
-export async function renderSlots(result: SSRResult, slots: any = {}): Promise {
+export async function renderSlots(result: SSRResult, slots: ComponentSlots = {}): Promise {
let slotInstructions: RenderSlotsResult['slotInstructions'] = null;
let children: RenderSlotsResult['children'] = {};
if (slots) {
await Promise.all(
Object.entries(slots).map(([key, value]) =>
- renderSlot(result, value as string).then((output: any) => {
+ renderSlot(result, value).then((output: any) => {
if (output.instructions) {
if (slotInstructions === null) {
slotInstructions = [];
diff --git a/packages/astro/src/runtime/server/render/types.ts b/packages/astro/src/runtime/server/render/types.ts
index 3aa3a16e3cef..a16ec034b32d 100644
--- a/packages/astro/src/runtime/server/render/types.ts
+++ b/packages/astro/src/runtime/server/render/types.ts
@@ -12,4 +12,10 @@ export type RenderHeadInstruction = {
result: SSRResult;
};
-export type RenderInstruction = RenderDirectiveInstruction | RenderHeadInstruction;
+export type MaybeRenderHeadInstruction = {
+ type: 'maybe-head';
+ result: SSRResult;
+ scope: number;
+}
+
+export type RenderInstruction = RenderDirectiveInstruction | RenderHeadInstruction | MaybeRenderHeadInstruction;
diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts
index 7e5ca9a5d160..a95ef16f8722 100644
--- a/packages/astro/src/runtime/server/render/util.ts
+++ b/packages/astro/src/runtime/server/render/util.ts
@@ -128,9 +128,3 @@ export function renderElement(
}
return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}${name}>`;
}
-
-export const ScopeFlags = {
- Astro: 1 << 0,
- JSX: 1 << 1,
- Slot: 1 << 2,
-};
diff --git a/packages/astro/src/vite-plugin-head-propagation/index.ts b/packages/astro/src/vite-plugin-head-propagation/index.ts
index ae8075e97e4f..eedefafd9ae9 100644
--- a/packages/astro/src/vite-plugin-head-propagation/index.ts
+++ b/packages/astro/src/vite-plugin-head-propagation/index.ts
@@ -1,8 +1,12 @@
import type { ModuleInfo } from 'rollup';
-import type { AstroSettings } from '../@types/astro';
+import type { AstroSettings, SSRResult } from '../@types/astro';
+import type { BuildInternals } from '../core/build/internal.js';
+import type { AstroBuildPlugin } from '../core/build/plugin.js';
+import type { StaticBuildOptions } from '../core/build/types';
import * as vite from 'vite';
import { getAstroMetadata } from '../vite-plugin-astro/index.js';
+import { walkParentInfos } from '../core/build/graph.js';
const injectExp = /^\/\/\s*astro-head-inject/;
/**
@@ -59,3 +63,50 @@ export default function configHeadPropagationVitePlugin({
},
};
}
+
+export function astroHeadPropagationBuildPlugin(
+ options: StaticBuildOptions,
+ internals: BuildInternals
+): AstroBuildPlugin {
+ return {
+ build: 'ssr',
+ hooks: {
+ 'build:before'() {
+ const map: SSRResult['propagation'] = new Map();
+ return {
+ vitePlugin: {
+ name: 'vite-plugin-head-propagation-build',
+ generateBundle(_opts, bundle) {
+ const appendPropagation = (info: ModuleInfo) => {
+ const astroMetadata = getAstroMetadata(info);
+ if(astroMetadata) {
+ astroMetadata.propagation = 'in-tree';
+ map.set(info.id, 'in-tree');
+ }
+ };
+
+ for(const [bundleId, output] of Object.entries(bundle)) {
+ if(output.type !== 'chunk') continue;
+ for(const [id, mod] of Object.entries(output.modules)) {
+ if (mod.code && injectExp.test(mod.code)) {
+ for(const [info] of walkParentInfos(id, this)) {
+ appendPropagation(info);
+ }
+ }
+
+ const info = this.getModuleInfo(id);
+ if(info) {
+ appendPropagation(info);
+ }
+ }
+ }
+
+ // Save the map to internals so it can be passed into SSR and generation
+ internals.propagation = map;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/integrations/mdx/test/css-head-mdx.test.js b/packages/integrations/mdx/test/css-head-mdx.test.js
index 2b1dcdfe7bcd..c38f23701d34 100644
--- a/packages/integrations/mdx/test/css-head-mdx.test.js
+++ b/packages/integrations/mdx/test/css-head-mdx.test.js
@@ -29,5 +29,32 @@ describe('Head injection w/ MDX', () => {
const scripts = document.querySelectorAll('head script[type=module]');
expect(scripts).to.have.a.lengthOf(1);
});
+
+ it('injects into the head for content collections', async () => {
+ const html = await fixture.readFile('/posts/test/index.html');
+ const { document } = parseHTML(html);
+
+ const links = document.querySelectorAll('head link[rel=stylesheet]');
+ expect(links).to.have.a.lengthOf(1);
+ });
+
+ it('injects content from a component using Content#render()', async () => {
+ const html = await fixture.readFile('/DirectContentUsage/index.html');
+ const { document } = parseHTML(html);
+
+ const links = document.querySelectorAll('head link[rel=stylesheet]');
+ expect(links).to.have.a.lengthOf(1);
+
+ const scripts = document.querySelectorAll('head script[type=module]');
+ expect(scripts).to.have.a.lengthOf(2);
+ });
+
+ it('Using component using slots.render() API', async () => {
+ const html = await fixture.readFile('/remote/index.html');
+ const { document } = parseHTML(html);
+
+ const links = document.querySelectorAll('head link[rel=stylesheet]');
+ expect(links).to.have.a.lengthOf(1);
+ });
});
});
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/package.json b/packages/integrations/mdx/test/fixtures/css-head-mdx/package.json
new file mode 100644
index 000000000000..3c3c1e5a572d
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@test/mdx-css-head-mdx",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*",
+ "@astrojs/mdx": "workspace:*",
+ "astro-remote": "0.2.3"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro
new file mode 100644
index 000000000000..071e08a12d42
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro
new file mode 100644
index 000000000000..a0bd6e1f1501
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro
@@ -0,0 +1,3 @@
+---
+---
+
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro
new file mode 100644
index 000000000000..1804388b0524
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro
@@ -0,0 +1,8 @@
+---
+import { getEntryBySlug } from 'astro:content';
+
+const launchWeek = await getEntryBySlug('blog', 'using-mdx');
+const { Content } = await launchWeek.render();
+---
+
+
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro
new file mode 100644
index 000000000000..0b8c4445f6fd
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro
@@ -0,0 +1,6 @@
+---
+---
+
+
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/_styles.css b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/_styles.css
new file mode 100644
index 000000000000..1379b29c06f0
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/_styles.css
@@ -0,0 +1,3 @@
+body {
+ color: red !important;
+}
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/using-mdx.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/using-mdx.mdx
new file mode 100644
index 000000000000..917fc3331288
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/blog/using-mdx.mdx
@@ -0,0 +1,6 @@
+import './_styles.css';
+import WithHoistedScripts from '../../components/WithHoistedScripts.astro';
+
+# Using mdx
+
+
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/posts/test.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/posts/test.mdx
new file mode 100644
index 000000000000..0bb1153cab7c
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/posts/test.mdx
@@ -0,0 +1,5 @@
+---
+title: Testing
+---
+
+A test file
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro
new file mode 100644
index 000000000000..7b234e86859a
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro
@@ -0,0 +1,24 @@
+---
+export interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+---
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro
new file mode 100644
index 000000000000..cbf4295a7149
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro
@@ -0,0 +1,17 @@
+---
+import UsingMdx from '../components/UsingMdx.astro'
+---
+
+
+
+
+
+
+
+ Astro
+
+
+ Astro
+
+
+
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro
new file mode 100644
index 000000000000..7d6ca0ca4d37
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro
@@ -0,0 +1,18 @@
+---
+import { getCollection } from 'astro:content';
+import Layout from '../../layouts/ContentLayout.astro';
+import SmallCaps from '../../components/SmallCaps.astro';
+
+export async function getStaticPaths() {
+ const entries = await getCollection('posts');
+ return entries.map(entry => {
+ return {params: { post: entry.slug }, props: { entry },
+ }});
+}
+
+const { entry } = Astro.props;
+const { Content } = await entry.render();
+---
+
+
+
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro
new file mode 100644
index 000000000000..9a7b76a10283
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro
@@ -0,0 +1,17 @@
+---
+import '../styles/global.css'
+import Layout from '../layouts/One.astro';
+import Paragraph from '../components/P.astro';
+import { Markdown } from 'astro-remote'
+---
+
+
+
+
+ **Removing p component fixes the problem**
+
+
+
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css
new file mode 100644
index 000000000000..e1450526f7a2
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css
@@ -0,0 +1,3 @@
+html {
+ font-weight: bolder;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8a83bba42ce7..aa398911206f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -375,7 +375,7 @@ importers:
packages/astro:
specifiers:
- '@astrojs/compiler': ^1.0.1
+ '@astrojs/compiler': ^1.1.0
'@astrojs/language-server': ^0.28.3
'@astrojs/markdown-remark': ^2.0.1
'@astrojs/telemetry': ^2.0.0
@@ -465,7 +465,7 @@ importers:
yargs-parser: ^21.0.1
zod: ^3.17.3
dependencies:
- '@astrojs/compiler': 1.0.1
+ '@astrojs/compiler': 1.1.0
'@astrojs/language-server': 0.28.3
'@astrojs/markdown-remark': link:../markdown/remark
'@astrojs/telemetry': link:../telemetry
@@ -2918,6 +2918,16 @@ importers:
remark-toc: 8.0.1
vite: 4.1.1
+ packages/integrations/mdx/test/fixtures/css-head-mdx:
+ specifiers:
+ '@astrojs/mdx': workspace:*
+ astro: workspace:*
+ astro-remote: 0.2.3
+ dependencies:
+ '@astrojs/mdx': link:../../..
+ astro: link:../../../../../astro
+ astro-remote: 0.2.3
+
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection:
specifiers:
'@astrojs/mdx': workspace:*
@@ -3857,8 +3867,8 @@ packages:
/@astrojs/compiler/0.31.4:
resolution: {integrity: sha512-6bBFeDTtPOn4jZaiD3p0f05MEGQL9pw2Zbfj546oFETNmjJFWO3nzHz6/m+P53calknCvyVzZ5YhoBLIvzn5iw==}
- /@astrojs/compiler/1.0.1:
- resolution: {integrity: sha512-77aacobLKcL98NmhK3OBS5EHIrX9gs1ckB/vGSIdkVZuB7u51V4jh05I6W0tSvG7/86tALv6QtHTRZ8rLhFTbQ==}
+ /@astrojs/compiler/1.1.0:
+ resolution: {integrity: sha512-C4kTwirys+HafufMqaxCbML2wqkGaXJM+5AekXh/v1IIOnMIdcEON9GBYsG6qa8aAmLhZ58aUZGPhzcA3Dx7Uw==}
dev: false
/@astrojs/language-server/0.28.3:
@@ -7837,6 +7847,14 @@ packages:
astro: link:packages/astro
dev: false
+ /astro-remote/0.2.3:
+ resolution: {integrity: sha512-vsY736YjWhpFgx4KUxCBdK0QJmOk0W61VQwO7v6qmfGdIxZyx6N7hBNou57w2mw68hQSe5AbRs602pi05GDMHw==}
+ dependencies:
+ he: 1.2.0
+ marked: 4.2.12
+ ultrahtml: 0.1.3
+ dev: false
+
/async-sema/3.1.1:
resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==}
dev: false
@@ -11279,6 +11297,12 @@ packages:
resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
dev: false
+ /marked/4.2.12:
+ resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==}
+ engines: {node: '>= 12'}
+ hasBin: true
+ dev: false
+
/matcher/3.0.0:
resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
engines: {node: '>=10'}
@@ -14735,6 +14759,10 @@ packages:
resolution: {integrity: sha512-o0QVGuFg24FK765Qdd5kk0zU/U4dEsCtN/GSiwNI9i8xsSVtjIAOdTaVhLwZ1nrbWxFVMxNDDl+9fednsOMsBw==}
dev: true
+ /ultrahtml/0.1.3:
+ resolution: {integrity: sha512-P24ulZdT9UKyQuKA1IApdAZ+F9lwruGvmKb4pG3+sMvR3CjN0pjawPnxuSABHQFB+XqnB35TVXzJPOBYjCv6Kw==}
+ dev: false
+
/unbox-primitive/1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
dependencies: