diff --git a/packages/myst-cli/src/build/html/index.ts b/packages/myst-cli/src/build/html/index.ts
index 01126785c..b7ab2d4a0 100644
--- a/packages/myst-cli/src/build/html/index.ts
+++ b/packages/myst-cli/src/build/html/index.ts
@@ -10,7 +10,7 @@
import fs from 'fs-extra';
import path from 'node:path';
-import { writeFileToFolder } from 'myst-cli-utils';
+import { writeFileToFolder, createNpmLogger, makeExecutable } from 'myst-cli-utils';
import type { MystXRefs } from 'myst-transforms';
import type { ISession } from '../../session/types.js';
import type { StartOptions } from '../site/start.js';
@@ -70,10 +70,12 @@ export async function currentSiteRoutes(
};
}),
// Download other assets
- ...['robots.txt', 'myst-theme.css', 'sitemap.xml', 'sitemap_style.xsl'].map((asset) => ({
- url: `${host}/${asset}`,
- path: asset,
- })),
+ ...['robots.txt', 'myst-theme.css', 'sitemap.xml', 'sitemap_style.xsl', 'objects.inv'].map(
+ (asset) => ({
+ url: `${host}/${asset}`,
+ path: asset,
+ }),
+ ),
{
url: `${host}/favicon.ico`,
path: 'favicon.ico',
@@ -167,6 +169,8 @@ function getBaseUrl(session: ISession): string | undefined {
*/
export async function buildHtml(session: ISession, opts: StartOptions) {
const template = await getSiteTemplate(session, opts);
+ // Ask template for command to render itself into HTML
+ const renderCommand = (template.getValidatedTemplateYml().build as any)?.render;
// The BASE_URL env variable allows for mounting the site in a folder, e.g., github pages
const baseurl = getBaseUrl(session);
// Note, this process is really only for Remix templates
@@ -174,57 +178,93 @@ export async function buildHtml(session: ISession, opts: StartOptions) {
const htmlDir = path.join(session.buildPath(), 'html');
fs.rmSync(htmlDir, { recursive: true, force: true });
fs.mkdirSync(htmlDir, { recursive: true });
- const appServer = await startServer(session, { ...opts, buildStatic: true, baseurl });
+ const appServer = await startServer(session, {
+ ...opts,
+ buildStatic: true,
+ baseurl,
+ // We do not need a running site if the template knows how to render itself
+ headless: renderCommand !== undefined,
+ });
if (!appServer) return;
+
const host = `http://localhost:${appServer.port}`;
- const routes = await currentSiteRoutes(session, host, baseurl);
- // Fetch all HTML pages and assets by the template
- await Promise.all(
- routes.map(async (route) =>
- limitConnections(async () => {
- try {
- const resp = await fetchWithRetry(session, route.url);
- if (!resp.ok) {
- session.log.error(`Error fetching ${route.url}`);
- return;
- }
- if (route.binary && resp.body) {
- await new Promise((resolve, reject) => {
- const filename = path.join(htmlDir, route.path);
- if (!fs.existsSync(filename))
- fs.mkdirSync(path.dirname(filename), { recursive: true });
- const fileWriteStream = fs.createWriteStream(filename);
- resp.body!.pipe(fileWriteStream);
- resp.body!.on('error', reject);
- fileWriteStream.on('error', reject);
- fileWriteStream.on('finish', resolve);
- });
- } else {
- const content = await resp.text();
- writeFileToFolder(path.join(htmlDir, route.path), content);
+ // Use the template to render itself
+ if (renderCommand !== undefined) {
+ // Run pre-rendering
+ await makeExecutable(renderCommand, createNpmLogger(session), {
+ cwd: template.templatePath,
+ env: { ...process.env, BUILD_DIRECTORY: htmlDir, CONTENT_CDN: host },
+ })();
+ }
+ // Fallback on fetch-based rendering (deprecated)
+ else {
+ const routes = await currentSiteRoutes(session, host, baseurl);
+
+ // Fetch all HTML pages and assets by the template
+ await Promise.all(
+ routes.map(async (route) =>
+ limitConnections(async () => {
+ try {
+ const resp = await fetchWithRetry(session, route.url);
+ if (!resp.ok) {
+ session.log.error(`Error fetching ${route.url}`);
+ return;
+ }
+ if (route.binary && resp.body) {
+ await new Promise((resolve, reject) => {
+ const filename = path.join(htmlDir, route.path);
+ if (!fs.existsSync(filename))
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
+ const fileWriteStream = fs.createWriteStream(filename);
+ resp.body!.pipe(fileWriteStream);
+ resp.body!.on('error', reject);
+ fileWriteStream.on('error', reject);
+ fileWriteStream.on('finish', resolve);
+ });
+ } else {
+ const content = await resp.text();
+ writeFileToFolder(path.join(htmlDir, route.path), content);
+ }
+ } catch (error) {
+ if (!route.optional) throw error;
+ session.log.warn(
+ `Could not fetch optional asset ${route.url}: ${(error as Error).message}`,
+ );
}
- } catch (error) {
- if (!route.optional) throw error;
- session.log.warn(
- `Could not fetch optional asset ${route.url}: ${(error as Error).message}`,
- );
- }
- }),
- ),
- );
- await appServer.stop();
+ }),
+ ),
+ );
- // Copy the files for the template used.
- //
- // This always includes the thebe JS chunks, even when no project enables
- // `thebe`/`jupyter`. The myst-theme uses thebe-core to render Jupyter cell
- // outputs, so these chunks are required for outputs to render at all.
- const templateBuildDir = path.join(template.templatePath, 'public');
- fs.copySync(templateBuildDir, htmlDir);
+ // Copy all of the static assets
+ fs.copySync(session.publicPath(), path.join(htmlDir, 'build'));
+ fs.copySync(
+ path.join(session.sitePath(), 'myst.search.json'),
+ path.join(htmlDir, 'myst.search.json'),
+ );
- // Copy all of the static assets
- fs.copySync(session.publicPath(), path.join(htmlDir, 'build'));
+ // NOTE: HTML static output needs to patch the contents, this is done on the fly by the server
+ const xrefs = JSON.parse(
+ fs.readFileSync(path.join(session.sitePath(), 'myst.xref.json')).toString(),
+ ) as MystXRefs;
+ xrefs.references?.forEach((ref) => {
+ ref.data = ref.data?.replace(/^\/content/, '');
+ });
+ fs.writeFileSync(path.join(htmlDir, 'myst.xref.json'), JSON.stringify(xrefs));
+
+ // Copy the files for the template used.
+ //
+ // This always includes the thebe JS chunks, even when no project enables
+ // `thebe`/`jupyter`. The myst-theme uses thebe-core to render Jupyter cell
+ // outputs, so these chunks are required for outputs to render at all.
+ const templateBuildDir = path.join(template.templatePath, 'public');
+ fs.copySync(templateBuildDir, htmlDir);
+ fs.copySync(path.join(session.sitePath(), 'config.json'), path.join(htmlDir, 'config.json'));
+
+ // We need to go through and change all links to the right folder
+ rewriteAssetsFolder(htmlDir, baseurl);
+ }
+ await appServer.stop();
// Copy user static files to html build root
const siteConfig = selectors.selectCurrentSiteConfig(session.store.getState());
@@ -233,27 +273,6 @@ export async function buildHtml(session: ISession, opts: StartOptions) {
const projectConfig = selectors.selectLocalProjectConfig(session.store.getState(), proj.path);
copyStaticFiles(session, projectConfig?.static_files ?? [], htmlDir, proj.path);
}
- fs.copySync(path.join(session.sitePath(), 'config.json'), path.join(htmlDir, 'config.json'));
- fs.copySync(path.join(session.sitePath(), 'public.json'), path.join(htmlDir, 'public.json'));
- fs.copySync(path.join(session.sitePath(), 'objects.inv'), path.join(htmlDir, 'objects.inv'));
-
- // NOTE: HTML static output needs to patch the contents, this is done on the fly by the server
- const xrefs = JSON.parse(
- fs.readFileSync(path.join(session.sitePath(), 'myst.xref.json')).toString(),
- ) as MystXRefs;
- xrefs.references?.forEach((ref) => {
- ref.data = ref.data?.replace(/^\/content/, '');
- });
- fs.writeFileSync(path.join(htmlDir, 'myst.xref.json'), JSON.stringify(xrefs));
-
- // Copy the search index
- fs.copySync(
- path.join(session.sitePath(), 'myst.search.json'),
- path.join(htmlDir, 'myst.search.json'),
- );
-
- // We need to go through and change all links to the right folder
- rewriteAssetsFolder(htmlDir, baseurl);
// Explicitly close the process as the web server doesn't always stop?
process.exit(0);