Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions lib/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const _ = require('lodash'),
ASYNC_SCRIPT_TAG = 'async',
DEFER_SCRIPT_TAG = 'defer',
ASYNC_DEFER_SCRIPT_TAG = 'async-defer',
MODULE_SCRIPT_TAG = 'module',
MODULEPRELOAD_TAG = 'modulepreload',
MEDIA_DIRECTORY = path.join(process.cwd(), 'public');

/**
Expand Down Expand Up @@ -152,6 +154,10 @@ function appendMediaToBottom(scripts, html) {

function injectTags(fileArray, site, tag) {
var buster = module.exports.cacheBuster ? `?version=${module.exports.cacheBuster}` : '';
// When omitCacheBusterOnModules is enabled, content-hashed ESM files omit the
// ?version= query string — the hash in the filename already provides cache busting.
// Defaults to false so existing deployments are unaffected.
var moduleBuster = module.exports.omitCacheBusterOnModules ? '' : buster;

return bluebird.resolve(_.map(fileArray, function (file) {
if (tag === STYLE_TAG) {
Expand All @@ -162,6 +168,12 @@ function injectTags(fileArray, site, tag) {
return `<script defer src="${file}" type="text/javascript"></script>`;
} else if (tag === ASYNC_DEFER_SCRIPT_TAG) {
return `<script async defer src="${file}" type="text/javascript"></script>`;
} else if (tag === MODULE_SCRIPT_TAG) {
return `<script type="module" src="${file}${moduleBuster}"></script>`;
} else if (tag === MODULEPRELOAD_TAG) {
// <link rel="modulepreload"> tells the browser to fetch ESM scripts early,
// during HTML parsing, before reaching the <script> tags at </body>.
return `<link rel="modulepreload" href="${file}${moduleBuster}">`;
} else {
return `<script type="text/javascript" src="${file}${buster}"></script>`;
}
Expand Down Expand Up @@ -274,6 +286,14 @@ function getStyleFiles(state) {
}
}

// check for clay compile linked? clay compile font files?
const defaultFontPath = path.join(assetDir, 'css', '_linked-fonts._default.css');
const siteFontPath = path.join(assetDir, 'css', `_linked-fonts.${siteStyleguide}.css`);

// add any default and site font css
cssFilePaths.push(defaultFontPath);
cssFilePaths.push(siteFontPath);

return cssFilePaths
.filter(files.fileExists)
.map(pathJoin(assetHost, assetPath, assetDir));
Expand Down Expand Up @@ -370,6 +390,14 @@ function configure(options, cacheBuster = '') {
if (options && _.isObject(options)) {
module.exports.editStylesTags = options.styles || false;
module.exports.editScriptsTags = options.scripts || false;
// modulepreload: when true, <link rel="modulepreload"> hints are injected
// into <head> for ESM scripts, and ?version= is omitted from module URLs
// since content-hashed filenames already provide cache busting.
// Opt-in only — defaults to false for backwards compatibility.
if (options.modulepreload !== undefined) {
module.exports.modulepreload = !!options.modulepreload;
module.exports.omitCacheBusterOnModules = !!options.modulepreload;
}
} else {
module.exports.editStylesTags = options;
module.exports.editScriptsTags = options;
Expand Down Expand Up @@ -404,8 +432,29 @@ function injectScriptsAndStyles(state) {
mediaMap = module.exports.getMediaMap(state);

// allow site to change the media map before applying it
// Expose rendered component names so resolveMedia can do per-component script resolution
// (e.g. pack-next manifest lookup) without needing a reference to the full state object.
locals._components = state._components;
if (setup.resolveMedia) mediaMap = setup.resolveMedia(mediaMap, locals) || mediaMap;

// moduleScripts: ESM scripts (e.g. from esbuild pack-next) that need type="module"
const moduleScriptFiles = mediaMap.moduleScripts || [];

mediaMap.moduleScripts = moduleScriptFiles.length
? injectTags(moduleScriptFiles, locals.site, MODULE_SCRIPT_TAG)
: bluebird.resolve(false);

// modulePreloads: <link rel="modulepreload"> hints for <head>.
// Only active when configure({ modulepreload: true }) has been called — opt-in so
// sites not using the clay build pipeline are completely unaffected.
const modulePreloadFiles = module.exports.modulepreload
? (mediaMap.modulePreloads || [])
: [];

mediaMap.modulePreloads = modulePreloadFiles.length
? injectTags(modulePreloadFiles, locals.site, MODULEPRELOAD_TAG)
: bluebird.resolve(false);

if (!locals.edit) {
mediaMap.styles = combineFileContents(mediaMap.styles, 'public/css', '/css/', STYLE_TAG);
mediaMap.scripts = !!mediaMap.manifestAssets && mediaMap.manifestAssets.length > 0
Expand All @@ -418,7 +467,11 @@ function injectScriptsAndStyles(state) {

return bluebird.props(mediaMap)
.then(combinedFiles => {
// modulepreload hints go first in <head>, before CSS, so the browser can
// start fetching ESM scripts at the earliest possible moment.
html = combinedFiles.modulePreloads ? appendMediaToTop(combinedFiles.modulePreloads, html) : html;
html = combinedFiles.styles ? appendMediaToTop(combinedFiles.styles, html) : html; // If there are styles, append them
html = combinedFiles.moduleScripts ? appendMediaToBottom(combinedFiles.moduleScripts, html) : html; // ESM module scripts (type="module")
html = combinedFiles.scripts ? appendMediaToBottom(combinedFiles.scripts, html) : html; // If there are scripts, append them
return html; // Return the compiled HTML
});
Expand All @@ -431,6 +484,10 @@ module.exports.cacheBuster = '';
module.exports.configure = configure;
module.exports.editStylesTags = false;
module.exports.editScriptsTags = false;
// Opt-in flags for the clay build (esbuild) pipeline.
// Enable via configure({ modulepreload: true }).
module.exports.modulepreload = false;
module.exports.omitCacheBusterOnModules = false;

// For testing
module.exports.getManifestAssets = getManifestAssets;
Expand Down
14 changes: 9 additions & 5 deletions lib/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function applyRenderHooks(ref, data, locals) {
* @param {Object} locals
* @returns {Function}
*/
function applyPostRenderHooks(ref, locals) {
function applyPostRenderHooks(ref, locals, res, self) {
return (html) => {

// skip postRender hooks in edit mode
Expand All @@ -50,7 +50,7 @@ function applyPostRenderHooks(ref, locals) {

return setup.plugins.filter((plugin) => plugin.postRender)
.reduce((val, plugin) => {
return plugin.postRender(ref, html, locals);
return plugin.postRender(ref, html, locals, res, self);
}, html);
};
}
Expand Down Expand Up @@ -148,7 +148,7 @@ function render(data, meta, res) {
return applyRenderHooks(state._layoutRef || state.self, data, state.locals)
.then(makeHtml(state))
.then(mediaService.injectScriptsAndStyles(state))
.then(applyPostRenderHooks(state._layoutRef || state.self, state.locals))
.then(applyPostRenderHooks(state._layoutRef || state.self, state.locals, res, meta._ref))
.then(result => {
res.type('text/html');
res.send(result);
Expand Down Expand Up @@ -182,8 +182,12 @@ function logTime(hrStart, msg, route) {
};
}

function configure({ editAssetTags, cacheBuster }) {
mediaService.configure(editAssetTags, cacheBuster);
function configure({ editAssetTags, cacheBuster, modulepreload }) {
const mediaOptions = editAssetTags && typeof editAssetTags === 'object'
? Object.assign({}, editAssetTags, modulepreload !== undefined ? { modulepreload } : {})
: editAssetTags;

mediaService.configure(mediaOptions, cacheBuster);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "amphora-html",
"version": "6.0.0-7",
"version": "6.0.1-dev.0",
"description": "An HTML renderer for component data",
"main": "index.js",
"scripts": {
Expand Down
Loading