diff --git a/.changeset/thirty-bugs-film.md b/.changeset/thirty-bugs-film.md new file mode 100644 index 000000000..9a8ba2cc9 --- /dev/null +++ b/.changeset/thirty-bugs-film.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix head injection misplacement with Astro.slots.render() diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts index 2750c886f..484f574c6 100644 --- a/packages/astro/src/runtime/server/render/common.ts +++ b/packages/astro/src/runtime/server/render/common.ts @@ -9,7 +9,7 @@ import { PrescriptType, } from '../scripts.js'; import { renderAllHeadContent } from './head.js'; -import { ScopeFlags } from './scope.js'; +import { hasScopeFlag, ScopeFlags } from './scope.js'; import { isSlotString, type SlotString } from './slot.js'; export const Fragment = Symbol.for('astro:fragment'); @@ -63,7 +63,23 @@ export function stringifyChunk(result: SSRResult, chunk: string | SlotString | R return ''; } - // Astro.slots.render('default') should never render head content. + // Astro rendered within JSX, head will be injected by the page itself. + case ScopeFlags.JSX | ScopeFlags.Astro: { + if(hasScopeFlag(result, ScopeFlags.JSX)) { + return ''; + } + break; + } + + // If the current scope is with Astro.slots.render() + case ScopeFlags.Slot: { + if(hasScopeFlag(result, ScopeFlags.RenderSlot)) { + return ''; + } + break; + } + + // Astro.slots.render() 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: { diff --git a/packages/astro/src/runtime/server/render/scope.ts b/packages/astro/src/runtime/server/render/scope.ts index 43cf80b51..fce96c0e7 100644 --- a/packages/astro/src/runtime/server/render/scope.ts +++ b/packages/astro/src/runtime/server/render/scope.ts @@ -18,6 +18,10 @@ export function removeScopeFlag(result: SSRResult, flag: ScopeFlagValues) { result.scope &= ~flag; } +export function hasScopeFlag(result: SSRResult, flag: ScopeFlagValues) { + return (result.scope & flag) === flag; +} + export function createScopedResult(result: SSRResult, flag?: ScopeFlagValues): SSRResult { const scopedResult = Object.create(result, { scope: { diff --git a/packages/astro/src/runtime/server/render/slot.ts b/packages/astro/src/runtime/server/render/slot.ts index 5e21d1e07..b0e8a0fa7 100644 --- a/packages/astro/src/runtime/server/render/slot.ts +++ b/packages/astro/src/runtime/server/render/slot.ts @@ -4,7 +4,7 @@ import type { RenderInstruction } from './types.js'; import { HTMLString, markHTMLString } from '../escape.js'; import { renderChild } from './any.js'; -import { createScopedResult, ScopeFlags } from './scope.js'; +import { createScopedResult, hasScopeFlag, ScopeFlags } from './scope.js'; type RenderTemplateResult = ReturnType; export type ComponentSlots = Record; diff --git a/packages/astro/test/fixtures/head-injection-md/package.json b/packages/astro/test/fixtures/head-injection/package.json similarity index 72% rename from packages/astro/test/fixtures/head-injection-md/package.json rename to packages/astro/test/fixtures/head-injection/package.json index d2f2c6778..82455011a 100644 --- a/packages/astro/test/fixtures/head-injection-md/package.json +++ b/packages/astro/test/fixtures/head-injection/package.json @@ -1,5 +1,5 @@ { - "name": "@test/head-injection-md", + "name": "@test/head-injection", "version": "0.0.0", "private": true, "dependencies": { diff --git a/packages/astro/test/fixtures/head-injection-md/src/components/Layout.astro b/packages/astro/test/fixtures/head-injection/src/components/Layout.astro similarity index 100% rename from packages/astro/test/fixtures/head-injection-md/src/components/Layout.astro rename to packages/astro/test/fixtures/head-injection/src/components/Layout.astro diff --git a/packages/astro/test/fixtures/head-injection/src/components/RegularSlot.astro b/packages/astro/test/fixtures/head-injection/src/components/RegularSlot.astro new file mode 100644 index 000000000..cec06fe2f --- /dev/null +++ b/packages/astro/test/fixtures/head-injection/src/components/RegularSlot.astro @@ -0,0 +1,8 @@ + +
+ +
diff --git a/packages/astro/test/fixtures/head-injection/src/components/SlotRenderComponent.astro b/packages/astro/test/fixtures/head-injection/src/components/SlotRenderComponent.astro new file mode 100644 index 000000000..d8756fff5 --- /dev/null +++ b/packages/astro/test/fixtures/head-injection/src/components/SlotRenderComponent.astro @@ -0,0 +1,12 @@ +--- +const html = await Astro.slots.render('slot-name'); +--- +
+ +
+ + diff --git a/packages/astro/test/fixtures/head-injection/src/components/SlotRenderLayout.astro b/packages/astro/test/fixtures/head-injection/src/components/SlotRenderLayout.astro new file mode 100644 index 000000000..efef491a0 --- /dev/null +++ b/packages/astro/test/fixtures/head-injection/src/components/SlotRenderLayout.astro @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/astro/test/fixtures/head-injection/src/components/SlotsRender.astro b/packages/astro/test/fixtures/head-injection/src/components/SlotsRender.astro new file mode 100644 index 000000000..85a57916e --- /dev/null +++ b/packages/astro/test/fixtures/head-injection/src/components/SlotsRender.astro @@ -0,0 +1,25 @@ +--- +export interface Props { + title: string; + subtitle: string; + content?: string; +} +const { + title, + subtitle = await Astro.slots.render("subtitle"), + content = await Astro.slots.render("content"), +} = Astro.props; +--- + + +
+
+ {title &&

{title}

} + {subtitle &&

} + {content &&

} +
+
diff --git a/packages/astro/test/fixtures/head-injection-md/src/pages/index.md b/packages/astro/test/fixtures/head-injection/src/pages/index.md similarity index 100% rename from packages/astro/test/fixtures/head-injection-md/src/pages/index.md rename to packages/astro/test/fixtures/head-injection/src/pages/index.md diff --git a/packages/astro/test/fixtures/head-injection/src/pages/with-slot-in-render-slot.astro b/packages/astro/test/fixtures/head-injection/src/pages/with-slot-in-render-slot.astro new file mode 100644 index 000000000..e3c2975e2 --- /dev/null +++ b/packages/astro/test/fixtures/head-injection/src/pages/with-slot-in-render-slot.astro @@ -0,0 +1,24 @@ +--- +import Layout from '../components/Layout.astro'; +import SlotsRender from '../components/SlotsRender.astro'; +--- + + + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. +

+ +

+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +

+
+
+
diff --git a/packages/astro/test/fixtures/head-injection/src/pages/with-slot-in-slot.astro b/packages/astro/test/fixtures/head-injection/src/pages/with-slot-in-slot.astro new file mode 100644 index 000000000..85b228c43 --- /dev/null +++ b/packages/astro/test/fixtures/head-injection/src/pages/with-slot-in-slot.astro @@ -0,0 +1,11 @@ +--- +import Layout from "../components/SlotRenderLayout.astro"; +import RegularSlot from "../components/RegularSlot.astro" +--- + + + +

Paragraph.

+
+
+
diff --git a/packages/astro/test/fixtures/head-injection/src/pages/with-slot-render.astro b/packages/astro/test/fixtures/head-injection/src/pages/with-slot-render.astro new file mode 100644 index 000000000..337b4a95c --- /dev/null +++ b/packages/astro/test/fixtures/head-injection/src/pages/with-slot-render.astro @@ -0,0 +1,9 @@ +--- +import Layout from "../components/SlotRenderLayout.astro"; +import Component from "../components/SlotRenderComponent.astro" +--- + + +

Paragraph.

+
+
diff --git a/packages/astro/test/head-injection-md.test.js b/packages/astro/test/head-injection-md.test.js deleted file mode 100644 index 1acd5e83b..000000000 --- a/packages/astro/test/head-injection-md.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import { expect } from 'chai'; -import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; - -describe('Head injection with markdown', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/head-injection-md/', - }); - }); - - describe('build', () => { - before(async () => { - await fixture.build(); - }); - - it('only injects head content once', async () => { - const html = await fixture.readFile(`/index.html`); - const $ = cheerio.load(html); - - expect($('link[rel=stylesheet]')).to.have.a.lengthOf(1); - }); - }); -}); diff --git a/packages/astro/test/head-injection.test.js b/packages/astro/test/head-injection.test.js new file mode 100644 index 000000000..6f6010773 --- /dev/null +++ b/packages/astro/test/head-injection.test.js @@ -0,0 +1,55 @@ +import { expect } from 'chai'; +import * as cheerio from 'cheerio'; +import { loadFixture } from './test-utils.js'; + +describe('Head injection', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/head-injection/', + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + describe('Markdown', () => { + it('only injects head content once', async () => { + const html = await fixture.readFile(`/index.html`); + const $ = cheerio.load(html); + + expect($('head link[rel=stylesheet]')).to.have.a.lengthOf(1); + }); + }); + + describe('Astro components', () => { + it('Using slots within slots', async () => { + const html = await fixture.readFile('/with-slot-in-slot/index.html'); + const $ = cheerio.load(html); + + expect($('head link[rel=stylesheet]')).to.have.a.lengthOf(1); + expect($('body link[rel=stylesheet]')).to.have.a.lengthOf(0); + }); + + it('Using slots with Astro.slots.render()', async () => { + const html = await fixture.readFile('/with-slot-render/index.html'); + const $ = cheerio.load(html); + + expect($('head link[rel=stylesheet]')).to.have.a.lengthOf(1); + expect($('body link[rel=stylesheet]')).to.have.a.lengthOf(0); + }); + + it('Using slots within slots using Astro.slots.render()', async () => { + const html = await fixture.readFile('/with-slot-in-render-slot/index.html'); + const $ = cheerio.load(html); + + expect($('head link[rel=stylesheet]')).to.have.a.lengthOf(2); + expect($('body link[rel=stylesheet]')).to.have.a.lengthOf(0); + }); + }); + }); +}); diff --git a/packages/integrations/mdx/test/css-head-mdx.test.js b/packages/integrations/mdx/test/css-head-mdx.test.js index c38f23701..82a86e5d2 100644 --- a/packages/integrations/mdx/test/css-head-mdx.test.js +++ b/packages/integrations/mdx/test/css-head-mdx.test.js @@ -3,6 +3,7 @@ import mdx from '@astrojs/mdx'; import { expect } from 'chai'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; +import * as cheerio from 'cheerio'; describe('Head injection w/ MDX', () => { let fixture; @@ -56,5 +57,17 @@ describe('Head injection w/ MDX', () => { const links = document.querySelectorAll('head link[rel=stylesheet]'); expect(links).to.have.a.lengthOf(1); }); + + it('Using component but no layout', async () => { + const html = await fixture.readFile('/noLayoutWithComponent/index.html'); + // Using cheerio here because linkedom doesn't support head tag injection + const $ = cheerio.load(html); + + const headLinks = $('head link[rel=stylesheet]'); + expect(headLinks).to.have.a.lengthOf(1); + + const bodyLinks = $('body link[rel=stylesheet]'); + expect(bodyLinks).to.have.a.lengthOf(0); + }); }); }); diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/noLayoutWithComponent.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/noLayoutWithComponent.mdx new file mode 100644 index 000000000..9d799d4db --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/noLayoutWithComponent.mdx @@ -0,0 +1,22 @@ +--- +title: 'Lorem' +description: 'Lorem ipsum dolor sit amet' +pubDate: 'Jul 02 2022' +--- + +import MyComponent from '../components/HelloWorld.astro'; + + +## Lorem + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +## Lorem 2 + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + + +## Lorem 3 + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c6538959..daf8f0ecf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1849,7 +1849,7 @@ importers: dependencies: astro: link:../../.. - packages/astro/test/fixtures/head-injection-md: + packages/astro/test/fixtures/head-injection: specifiers: astro: workspace:* dependencies: