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:
Matthew Phillips 2022-08-15 13:10:28 -04:00 committed by GitHub
parent 655d9840f8
commit b55f76c1ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 101 additions and 7 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix double injecting of head content in md pages

View file

@ -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> {

View file

@ -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';

View file

@ -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;
} }

View file

@ -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;
`); `);

View file

@ -0,0 +1,8 @@
{
"name": "@test/head-injection-md",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View 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>

View file

@ -0,0 +1,7 @@
---
layout: ../components/Layout.astro
---
# Heading
And content here.

View 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);
});
});
});

View file

@ -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:*