Fix double injecting of head content in md pages (#4334)
* Fix double injecting of head content * Refactor * Changeset * Break into a separate util fn * fix oops * remove unused code
This commit is contained in:
parent
655d9840f8
commit
b55f76c1ca
10 changed files with 101 additions and 7 deletions
5
.changeset/hip-bobcats-divide.md
Normal file
5
.changeset/hip-bobcats-divide.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix double injecting of head content in md pages
|
|
@ -56,6 +56,10 @@ export function isAstroComponent(obj: any): obj is AstroComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAstroComponentFactory(obj: any): obj is AstroComponentFactory {
|
||||||
|
return obj == null ? false : !!obj.isAstroComponentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
export async function* renderAstroComponent(
|
export async function* renderAstroComponent(
|
||||||
component: InstanceType<typeof AstroComponent>
|
component: InstanceType<typeof AstroComponent>
|
||||||
): AsyncIterable<string | RenderInstruction> {
|
): AsyncIterable<string | RenderInstruction> {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { extractDirectives, generateHydrateScript } from '../hydration.js';
|
||||||
import { serializeProps } from '../serialize.js';
|
import { serializeProps } from '../serialize.js';
|
||||||
import { shorthash } from '../shorthash.js';
|
import { shorthash } from '../shorthash.js';
|
||||||
import { renderSlot } from './any.js';
|
import { renderSlot } from './any.js';
|
||||||
import { renderAstroComponent, renderTemplate, renderToIterable } from './astro.js';
|
import { isAstroComponentFactory, renderAstroComponent, renderTemplate, renderToIterable } from './astro.js';
|
||||||
import { Fragment, Renderer } from './common.js';
|
import { Fragment, Renderer } from './common.js';
|
||||||
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
|
import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
|
||||||
import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
|
import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
|
||||||
|
@ -37,7 +37,7 @@ function getComponentType(Component: unknown): ComponentType {
|
||||||
if (Component && typeof Component === 'object' && (Component as any)['astro:html']) {
|
if (Component && typeof Component === 'object' && (Component as any)['astro:html']) {
|
||||||
return 'html';
|
return 'html';
|
||||||
}
|
}
|
||||||
if (Component && (Component as any).isAstroComponentFactory) {
|
if (isAstroComponentFactory(Component)) {
|
||||||
return 'astro-factory';
|
return 'astro-factory';
|
||||||
}
|
}
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
|
|
|
@ -2,21 +2,34 @@ import type { SSRResult } from '../../../@types/astro';
|
||||||
import type { AstroComponentFactory } from './index';
|
import type { AstroComponentFactory } from './index';
|
||||||
|
|
||||||
import { createResponse } from '../response.js';
|
import { createResponse } from '../response.js';
|
||||||
import { isAstroComponent, renderAstroComponent } from './astro.js';
|
import { isAstroComponent, isAstroComponentFactory, renderAstroComponent } from './astro.js';
|
||||||
import { stringifyChunk } from './common.js';
|
import { stringifyChunk } from './common.js';
|
||||||
import { renderComponent } from './component.js';
|
import { renderComponent } from './component.js';
|
||||||
import { maybeRenderHead } from './head.js';
|
import { maybeRenderHead } from './head.js';
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
|
||||||
|
|
||||||
|
type NonAstroPageComponent = {
|
||||||
|
name: string;
|
||||||
|
[needsHeadRenderingSymbol]: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function nonAstroPageNeedsHeadInjection(pageComponent: NonAstroPageComponent): boolean {
|
||||||
|
return (
|
||||||
|
(needsHeadRenderingSymbol in pageComponent) &&
|
||||||
|
!!pageComponent[needsHeadRenderingSymbol]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function renderPage(
|
export async function renderPage(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
componentFactory: AstroComponentFactory,
|
componentFactory: AstroComponentFactory | NonAstroPageComponent,
|
||||||
props: any,
|
props: any,
|
||||||
children: any,
|
children: any,
|
||||||
streaming: boolean
|
streaming: boolean
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
if (!componentFactory.isAstroComponentFactory) {
|
if (!isAstroComponentFactory(componentFactory)) {
|
||||||
const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
|
const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
|
||||||
const output = await renderComponent(
|
const output = await renderComponent(
|
||||||
result,
|
result,
|
||||||
|
@ -29,8 +42,13 @@ export async function renderPage(
|
||||||
if (!/<!doctype html/i.test(html)) {
|
if (!/<!doctype html/i.test(html)) {
|
||||||
let rest = html;
|
let rest = html;
|
||||||
html = `<!DOCTYPE html>`;
|
html = `<!DOCTYPE html>`;
|
||||||
for await (let chunk of maybeRenderHead(result)) {
|
// This symbol currently exists for md components, but is something that could
|
||||||
html += chunk;
|
// be added for any page-level component that's not an Astro component.
|
||||||
|
// to signal that head rendering is needed.
|
||||||
|
if(nonAstroPageNeedsHeadInjection(componentFactory)) {
|
||||||
|
for await (let chunk of maybeRenderHead(result)) {
|
||||||
|
html += chunk;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
html += rest;
|
html += rest;
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,7 @@ export default function markdown({ config, logging }: AstroPluginOptions): Plugi
|
||||||
: `contentFragment`
|
: `contentFragment`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Content[Symbol.for('astro.needsHeadRendering')] = ${layout ? 'false' : 'true'};
|
||||||
export default Content;
|
export default Content;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|
8
packages/astro/test/fixtures/head-injection-md/package.json
vendored
Normal file
8
packages/astro/test/fixtures/head-injection-md/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/head-injection-md",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
18
packages/astro/test/fixtures/head-injection-md/src/components/Layout.astro
vendored
Normal file
18
packages/astro/test/fixtures/head-injection-md/src/components/Layout.astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
const title = 'My Title';
|
||||||
|
---
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
<head>
|
||||||
|
<title set:html={title}></title>
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex flex-col min-h-screen font-sans bg-indigo-50 dark:bg-slate-900">
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
7
packages/astro/test/fixtures/head-injection-md/src/pages/index.md
vendored
Normal file
7
packages/astro/test/fixtures/head-injection-md/src/pages/index.md
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
layout: ../components/Layout.astro
|
||||||
|
---
|
||||||
|
|
||||||
|
# Heading
|
||||||
|
|
||||||
|
And content here.
|
27
packages/astro/test/head-injection-md.test.js
Normal file
27
packages/astro/test/head-injection-md.test.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1531,6 +1531,12 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/head-injection-md:
|
||||||
|
specifiers:
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
astro: link:../../..
|
||||||
|
|
||||||
packages/astro/test/fixtures/hmr-css:
|
packages/astro/test/fixtures/hmr-css:
|
||||||
specifiers:
|
specifiers:
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
|
|
Loading…
Reference in a new issue