diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 9058daf0a..943c287f6 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -172,7 +172,12 @@ export function updateSiteManifestStaticLinksInplace( if (data.options.logo) data.options.logo = updateUrl(data.options.logo); if (data.options.logo_dark) data.options.logo_dark = updateUrl(data.options.logo_dark); if (data.options.favicon) data.options.favicon = updateUrl(data.options.favicon); - if (data.options.style) data.options.style = updateUrl(data.options.style); + if (data.options.style) { + // `style` may be a single link or, for a `multiple` file option, a list. + data.options.style = Array.isArray(data.options.style) + ? data.options.style.map(updateUrl) + : updateUrl(data.options.style); + } if (data.parts) { Object.values(data.parts).forEach(({ mdast }) => { updateMdastStaticLinksInplace(mdast, updateUrl); diff --git a/themes/article/app/utils/loaders.server.ts b/themes/article/app/utils/loaders.server.ts index 432d031d8..f5beba2bc 100644 --- a/themes/article/app/utils/loaders.server.ts +++ b/themes/article/app/utils/loaders.server.ts @@ -149,12 +149,25 @@ export async function getFavicon(): Promise<{ export async function getCustomStyleSheet(): Promise { // We are always fetching this at run time, so we don't want the rewritten links const config = await getConfig({ rewriteStaticFolder: false }); - const url = config.options?.style; - if (!url) { + // `style` may be a single URL or, since the option is declared `multiple`, a + // list of them. Normalize to an array, fetch each, and concatenate in order + // so later stylesheets override earlier ones. + const style = config.options?.style; + const urls = (Array.isArray(style) ? style : [style]).filter(Boolean); + if (urls.length === 0) { return; } - const response = await fetch(url).catch(() => null); - if (!response || response.status === 404) return; - const css = await response.text(); - return css; + const sheets = await Promise.all( + urls.map(async (url) => { + const response = await fetch(url).catch(() => null); + if (!response || response.status === 404) return undefined; + try { + return response.text(); + } catch { + return undefined; + } + }), + ); + const css = sheets.filter(Boolean).join('\n\n'); + return css || undefined; } diff --git a/themes/article/template.yml b/themes/article/template.yml index 19b294c62..c33d617be 100644 --- a/themes/article/template.yml +++ b/themes/article/template.yml @@ -71,7 +71,8 @@ options: Supports wildcard subdomains with *. Example: "jupyter.org" or "*.jupyter.org" - type: file id: style - description: Local path to a CSS file + multiple: true + description: URL to fetch, or a list of them (applied in order) build: install: npm ci --ignore-scripts start: npm run start diff --git a/themes/book/app/utils/loaders.server.ts b/themes/book/app/utils/loaders.server.ts index 81a657b37..bbebb7ab2 100644 --- a/themes/book/app/utils/loaders.server.ts +++ b/themes/book/app/utils/loaders.server.ts @@ -150,12 +150,25 @@ export async function getFavicon(): Promise<{ export async function getCustomStyleSheet(): Promise { // We are always fetching this at run time, so we don't want the rewritten links const config = await getConfig({ rewriteStaticFolder: false }); - const url = config.options?.style; - if (!url) { + // `style` may be a single URL or, since the option is declared `multiple`, a + // list of them. Normalize to an array, fetch each, and concatenate in order + // so later stylesheets override earlier ones. + const style = config.options?.style; + const urls = (Array.isArray(style) ? style : [style]).filter(Boolean); + if (urls.length === 0) { return; } - const response = await fetch(url).catch(() => null); - if (!response || response.status === 404) return; - const css = await response.text(); - return css; + const sheets = await Promise.all( + urls.map(async (url) => { + const response = await fetch(url).catch(() => null); + if (!response || response.status === 404) return undefined; + try { + return response.text(); + } catch { + return undefined; + } + }), + ); + const css = sheets.filter(Boolean).join('\n\n'); + return css || undefined; } diff --git a/themes/book/template.yml b/themes/book/template.yml index a10976462..8bf2c9178 100644 --- a/themes/book/template.yml +++ b/themes/book/template.yml @@ -77,7 +77,8 @@ options: Supports wildcard subdomains with *. Example: "jupyter.org" or "*.jupyter.org" - type: file id: style - description: Local path to a CSS file + multiple: true + description: URL to fetch, or a list of them (applied in order) parts: - id: footer description: The site wide footer