Skip to content

Commit

Permalink
Improve head injection behavior (#2436)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
natemoo-re authored and Nate Moore committed Mar 3, 2022
1 parent 9a92081 commit c64c127
Show file tree
Hide file tree
Showing 7 changed files with 38 additions and 57 deletions.
2 changes: 1 addition & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 0 additions & 11 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
9 changes: 0 additions & 9 deletions packages/astro/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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')
Expand Down
11 changes: 9 additions & 2 deletions packages/astro/src/core/render/core.ts
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';
Expand Down Expand Up @@ -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)) {
Expand Down
50 changes: 24 additions & 26 deletions packages/astro/src/runtime/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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';
Expand All @@ -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 },
Expand All @@ -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>) {
Expand Down
4 changes: 0 additions & 4 deletions packages/astro/src/vite-plugin-astro/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
Expand All @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down

0 comments on commit c64c127

Please sign in to comment.