Improve head
injection behavior (#2436)
* feat: add renderHead util to server * feat: remove `layouts` from config, Vite plugin * fix: improve head injection during rendering * chore: update compiler * fix: do not escape links
This commit is contained in:
parent
34c2d1e8fc
commit
7a66ddc60a
7 changed files with 38 additions and 57 deletions
|
@ -58,7 +58,7 @@
|
|||
"test:match": "mocha --timeout 20000 -g"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^0.12.0-next.5",
|
||||
"@astrojs/compiler": "^0.12.0-next.8",
|
||||
"@astrojs/language-server": "^0.8.6",
|
||||
"@astrojs/markdown-remark": "^0.6.4",
|
||||
"@astrojs/prism": "0.4.0",
|
||||
|
|
|
@ -515,17 +515,6 @@ export type Params = Record<string, string | undefined>;
|
|||
|
||||
export type Props = Record<string, unknown>;
|
||||
|
||||
export interface RenderPageOptions {
|
||||
request: {
|
||||
params?: Params;
|
||||
url: URL;
|
||||
canonicalURL: URL;
|
||||
};
|
||||
children: any[];
|
||||
props: Props;
|
||||
css?: string[];
|
||||
}
|
||||
|
||||
type Body = string;
|
||||
|
||||
export interface EndpointOutput<Output extends Body = Body> {
|
||||
|
|
|
@ -26,11 +26,6 @@ export const AstroConfigSchema = z.object({
|
|||
.optional()
|
||||
.default('./src/pages')
|
||||
.transform((val) => new URL(val)),
|
||||
layouts: z
|
||||
.string()
|
||||
.optional()
|
||||
.default('./src/layouts')
|
||||
.transform((val) => new URL(val)),
|
||||
public: z
|
||||
.string()
|
||||
.optional()
|
||||
|
@ -99,10 +94,6 @@ export async function validateConfig(userConfig: any, root: string): Promise<Ast
|
|||
.string()
|
||||
.default('./src/pages')
|
||||
.transform((val) => new URL(addTrailingSlash(val), fileProtocolRoot)),
|
||||
layouts: z
|
||||
.string()
|
||||
.default('./src/layouts')
|
||||
.transform((val) => new URL(addTrailingSlash(val), fileProtocolRoot)),
|
||||
public: z
|
||||
.string()
|
||||
.default('./public')
|
||||
|
|
|
@ -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-->", '');
|
||||
|
||||
// inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
|
||||
if (!legacyBuild && !/<!doctype html/i.test(html)) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
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) {
|
||||
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-->');
|
||||
}
|
||||
|
||||
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {
|
||||
|
|
|
@ -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',
|
||||
pathname,
|
||||
projectRoot: config.projectRoot.toString(),
|
||||
site: config.buildOptions.site,
|
||||
|
|
|
@ -137,10 +137,10 @@
|
|||
jsonpointer "^5.0.0"
|
||||
leven "^3.1.0"
|
||||
|
||||
"@astrojs/compiler@^0.12.0-next.5":
|
||||
version "0.12.0-next.5"
|
||||
resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.12.0-next.5.tgz#4e6d27c74787777522395018f2497dab4a032c77"
|
||||
integrity sha512-4YVPRrB9JJhxoNC9PWN2zpGE7SXRAXcyCouawbd24iyBl4g9aRoQN12XA0qQZkbea9/NNLe9f2yhFMubM2CrJQ==
|
||||
"@astrojs/compiler@^0.12.0-next.8":
|
||||
version "0.12.0-next.8"
|
||||
resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-0.12.0-next.8.tgz#a15792791790aaeeaf944797635a40a56f806975"
|
||||
integrity sha512-HeREaw5OR5J7zML+/LxhrqUr57571kyNXL4HD2pU929oevhx3PQ37PQ0FkD5N65X9YfO+gcoEO6whl76vtSZag==
|
||||
dependencies:
|
||||
typescript "^4.3.5"
|
||||
|
||||
|
|
Loading…
Reference in a new issue