-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve head
injection behavior
#2436
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro'; | ||
import type { LogOptions } from '../logger.js'; | ||
|
||
import { renderEndpoint, renderPage } from '../../runtime/server/index.js'; | ||
import { renderEndpoint, renderHead, renderToString } from '../../runtime/server/index.js'; | ||
import { getParams } from '../routing/index.js'; | ||
import { createResult } from './result.js'; | ||
import { findPathItemByKey, RouteCache, callGetStaticPaths } from './route-cache.js'; | ||
|
@@ -97,7 +97,14 @@ export async function render(opts: RenderOptions): Promise<string> { | |
scripts, | ||
}); | ||
|
||
let html = await renderPage(result, Component, pageProps, null); | ||
let html = await renderToString(result, Component, pageProps, null); | ||
|
||
// handle final head injection if it hasn't happened already | ||
if (html.indexOf("<!--astro:head:injected-->") == -1) { | ||
html = await renderHead(result) + html; | ||
} | ||
// cleanup internal state flags | ||
html = html.replace("<!--astro:head:injected-->", ''); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove our flag for a nicer output. |
||
|
||
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?) | ||
if (!legacyBuild && !/<!doctype html/i.test(html)) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,7 +71,7 @@ export class AstroComponent { | |
const html = htmlParts[i]; | ||
const expression = expressions[i]; | ||
|
||
yield _render(unescapeHTML(html)); | ||
yield unescapeHTML(html); | ||
yield _render(expression); | ||
} | ||
} | ||
|
@@ -405,20 +405,6 @@ export function defineScriptVars(vars: Record<any, any>) { | |
return output; | ||
} | ||
|
||
// Calls a component and renders it into a string of HTML | ||
export async function renderToString(result: SSRResult, componentFactory: AstroComponentFactory, props: any, children: any) { | ||
const Component = await componentFactory(result, props, children); | ||
let template = await renderAstroComponent(Component); | ||
return unescapeHTML(template); | ||
} | ||
|
||
// Filter out duplicate elements in our set | ||
const uniqueElements = (item: any, index: number, all: any[]) => { | ||
const props = JSON.stringify(item.props); | ||
const children = item.children; | ||
return index === all.findIndex((i) => JSON.stringify(i.props) === props && i.children == children); | ||
}; | ||
|
||
// Renders an endpoint request to completion, returning the body. | ||
export async function renderEndpoint(mod: EndpointHandler, params: any) { | ||
const method = 'get'; | ||
|
@@ -433,15 +419,34 @@ export async function renderEndpoint(mod: EndpointHandler, params: any) { | |
return body; | ||
} | ||
|
||
// Calls a component and renders it into a string of HTML | ||
export async function renderToString(result: SSRResult, componentFactory: AstroComponentFactory, props: any, children: any) { | ||
const Component = await componentFactory(result, props, children); | ||
let template = await renderAstroComponent(Component); | ||
|
||
// <!--astro:head--> injected by compiler | ||
// Must be handled at the end of the rendering process | ||
if (template.indexOf('<!--astro:head-->') > -1) { | ||
template = template.replace('<!--astro:head-->', await renderHead(result)); | ||
} | ||
Comment on lines
+429
to
+431
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return template; | ||
} | ||
|
||
// Filter out duplicate elements in our set | ||
const uniqueElements = (item: any, index: number, all: any[]) => { | ||
const props = JSON.stringify(item.props); | ||
const children = item.children; | ||
return index === all.findIndex((i) => JSON.stringify(i.props) === props && i.children == children); | ||
}; | ||
|
||
|
||
// Renders a page to completion by first calling the factory callback, waiting for its result, and then appending | ||
// styles and scripts into the head. | ||
export async function renderPage(result: SSRResult, Component: AstroComponentFactory, props: any, children: any) { | ||
const template = await renderToString(result, Component, props, children); | ||
export async function renderHead(result: SSRResult) { | ||
Comment on lines
-438
to
+445
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No more |
||
const styles = Array.from(result.styles) | ||
.filter(uniqueElements) | ||
.map((style) => { | ||
const styleChildren = !result._metadata.legacyBuild ? '' : style.children; | ||
|
||
return renderElement('style', { | ||
children: styleChildren, | ||
props: { ...style.props, 'astro-style': true }, | ||
|
@@ -462,17 +467,10 @@ export async function renderPage(result: SSRResult, Component: AstroComponentFac | |
if (needsHydrationStyles) { | ||
styles.push(renderElement('style', { props: { 'astro-style': true }, children: 'astro-root, astro-fragment { display: contents; }' })); | ||
} | ||
|
||
const links = Array.from(result.links) | ||
.filter(uniqueElements) | ||
.map((link) => renderElement('link', link, false)); | ||
|
||
// inject styles & scripts at end of <head> | ||
let headPos = template.indexOf('</head>'); | ||
if (headPos === -1) { | ||
return links.join('\n') + styles.join('\n') + scripts.join('\n') + template; // if no </head>, prepend styles & scripts | ||
} | ||
return template.substring(0, headPos) + links.join('\n') + styles.join('\n') + scripts.join('\n') + template.substring(headPos); | ||
return unescapeHTML(links.join('\n') + styles.join('\n') + scripts.join('\n') + '\n' + '<!--astro:head:injected-->'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I understand after reading the code below. |
||
} | ||
|
||
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,11 +33,8 @@ function safelyReplaceImportPlaceholder(code: string) { | |
const configCache = new WeakMap<AstroConfig, CompilationCache>(); | ||
|
||
async function compile(config: AstroConfig, filename: string, source: string, viteTransform: TransformHook, opts: { ssr: boolean }): Promise<CompileResult> { | ||
// pages and layouts should be transformed as full documents (implicit <head> <body> etc) | ||
// everything else is treated as a fragment | ||
const filenameURL = new URL(`file://${filename}`); | ||
const normalizedID = fileURLToPath(filenameURL); | ||
const isPage = normalizedID.startsWith(fileURLToPath(config.pages)) || normalizedID.startsWith(fileURLToPath(config.layouts)); | ||
const pathname = filenameURL.pathname.substr(config.projectRoot.pathname.length - 1); | ||
|
||
let rawCSSDeps = new Set<string>(); | ||
|
@@ -47,7 +44,6 @@ async function compile(config: AstroConfig, filename: string, source: string, vi | |
// use `sourcemap: "both"` so that sourcemap is included in the code | ||
// result passed to esbuild, but also available in the catch handler. | ||
const transformResult = await transform(source, { | ||
as: isPage ? 'document' : 'fragment', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No more |
||
pathname, | ||
projectRoot: config.projectRoot.toString(), | ||
site: config.buildOptions.site, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slight deviation from RFC0007... if we haven't injected any head elements yet, the user may not have rendered a
<head>
element so we just prepend them to the file.