Skip to content
Draft
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
2 changes: 2 additions & 0 deletions packages/common-types/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ export const defaultTheme = {
logo: undefined,
bodyFontFamily: 'Nunito',
bodyFontFamilyCss: "@font-face{font-family:Nunito;font-style:italic;font-weight:200 1000;font-display:swap;src:url({SITE_PATH}/simple-directory/fonts/XRXX3I6Li01BKofIMNaORs71cA-Bm_i0Dk1.woff2) format('woff2');unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Nunito;font-style:italic;font-weight:200 1000;font-display:swap;src:url({SITE_PATH}/simple-directory/fonts/XRXX3I6Li01BKofIMNaHRs71cA-Cznx39fA.woff2) format('woff2');unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Nunito;font-style:italic;font-weight:200 1000;font-display:swap;src:url({SITE_PATH}/simple-directory/fonts/XRXX3I6Li01BKofIMNaMRs71cA-CuWrHpFO.woff2) format('woff2');unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Nunito;font-style:italic;font-weight:200 1000;font-display:swap;src:url({SITE_PATH}/simple-directory/fonts/XRXX3I6Li01BKofIMNaNRs71cA-D1eeM49Z.woff2) format('woff2');unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Nunito;font-style:italic;font-weight:200 1000;font-display:swap;src:url({SITE_PATH}/simple-directory/fonts/XRXX3I6Li01BKofIMNaDRs4-BbMn9XSX.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Nunito;font-style:normal;font-weight:200 1000;font-display:swap;src:url({SITE_PATH}/simple-directory/fonts/XRXV3I6Li01BKofIOOaBXso-BWI5zH9R.woff2) format('woff2');unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Nunito;font-style:normal;font-weight:200 1000;font-display:swap;src:url({SITE_PATH}/simple-directory/fonts/XRXV3I6Li01BKofIMeaBXso-C3IBG1kp.woff2) format('woff2');unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Nunito;font-style:normal;font-weight:200 1000;font-display:swap;src:url({SITE_PATH}/simple-directory/fonts/XRXV3I6Li01BKofIOuaBXso-B55YuedR.woff2) format('woff2');unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Nunito;font-style:normal;font-weight:200 1000;font-display:swap;src:url({SITE_PATH}/simple-directory/fonts/XRXV3I6Li01BKofIO-aBXso-DcJfvmGA.woff2) format('woff2');unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Nunito;font-style:normal;font-weight:200 1000;font-display:swap;src:url({SITE_PATH}/simple-directory/fonts/XRXV3I6Li01BKofINeaB-BaTF6Vo7.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}",
bodyFontPreloadUrls: ['{SITE_PATH}/simple-directory/fonts/XRXV3I6Li01BKofINeaB-BaTF6Vo7.woff2'],
headingFontFamily: undefined,
headingFontFamilyCss: undefined,
headingFontPreloadUrls: undefined,
colors: {
// standard vuetify colors, see https://vuetifyjs.com/en/styles/colors/#material-colors
background: '#FAFAFA', // grey-lighten-5
Expand Down
44 changes: 37 additions & 7 deletions packages/common-types/theme/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,21 @@ export default {
},
type: 'string'
},
bodyFontPreloadUrls: {
title: 'URLs de préchargement de la police du corps du texte',
'x-i18n-title': {
fr: 'URLs de préchargement de la police du corps du texte',
en: 'Body font preload URLs',
es: 'URLs de precarga de la fuente del cuerpo',
it: 'URL di precaricamento del carattere del corpo',
pt: 'URLs de pré-carregamento da fonte do corpo',
de: 'URLs zum Vorladen der Fließtextschriftart'
},
description: 'Liste des URLs de fichiers de police (woff2, etc.) à précharger pour le corps du texte. Généralement renseignée automatiquement.',
type: 'array',
items: { type: 'string' },
layout: 'none'
},
headingFontFamily: {
title: 'Nom de police de caractères pour les titres',
'x-i18n-title': {
Expand Down Expand Up @@ -241,6 +256,21 @@ export default {
},
type: 'string'
},
headingFontPreloadUrls: {
title: 'URLs de préchargement de la police des titres',
'x-i18n-title': {
fr: 'URLs de préchargement de la police des titres',
en: 'Heading font preload URLs',
es: 'URLs de precarga de la fuente de los títulos',
it: 'URL di precaricamento del carattere delle intestazioni',
pt: 'URLs de pré-carregamento da fonte dos títulos',
de: 'URLs zum Vorladen der Überschriftenschriftart'
},
description: 'Liste des URLs de fichiers de police (woff2, etc.) à précharger pour les titres. Généralement renseignée automatiquement.',
type: 'array',
items: { type: 'string' },
layout: 'none'
},
assistedMode: {
type: 'boolean',
title: 'Mode de gestion des couleurs simplifié',
Expand All @@ -252,14 +282,14 @@ export default {
pt: 'Modo de gerenciamento simplificado de cores',
de: 'Vereinfachter Farbverwaltungsmodus'
},
description: 'Ce mode permet de simplifier la gestion des couleurs. Avec, vous ne renseignez que les couleurs principales (primaire, secondaire, accentuée).\n\nLes autres couleurs sont calculées automatiquement en suivant des règles de contraste pour rester lisibles sur les fonds.\n\n**Attention** : activer ce mode réinitialise les couleurs détaillées définies manuellement en mode avancé.',
description: 'Ce mode permet de simplifier la gestion des couleurs. Avec, vous ne renseignez que les couleurs principales (primaire, secondaire, accentuée).\n\nLes autres couleurs sont calculées automatiquement en suivant des règles de contraste pour rester lisibles sur les fonds.\n\n**Attention** : activer ce mode réinitialise les couleurs avancées définies manuellement.',
'x-i18n-description': {
fr: 'Ce mode permet de simplifier la gestion des couleurs. Avec, vous ne renseignez que les couleurs principales (primaire, secondaire, accentuée).\n\nLes autres couleurs sont calculées automatiquement en suivant des règles de contraste pour rester lisibles sur les fonds.\n\n**Attention** : activer ce mode réinitialise les couleurs détaillées définies manuellement en mode avancé.',
en: 'This mode simplifies color management: you only provide the main colors (primary, secondary, accent).\n\nThe other colors are computed automatically using contrast rules so they remain legible on backgrounds.\n\n**Warning**: enabling this mode resets any detailed colors that were set manually in advanced mode.',
es: 'Este modo simplifica la gestión de colores: solo indicas los colores principales (primario, secundario, acentuado).\n\nLos demás colores se calculan automáticamente siguiendo reglas de contraste para que sigan siendo legibles sobre los fondos.\n\n**Atención**: activar este modo restablece los colores detallados que se definieron manualmente en el modo avanzado.',
it: 'Questa modalità semplifica la gestione dei colori: fornisci solo i colori principali (primario, secondario, accento).\n\nGli altri colori vengono calcolati automaticamente seguendo regole di contrasto per restare leggibili sugli sfondi.\n\n**Attenzione**: abilitando questa modalità vengono reimpostati i colori dettagliati definiti manualmente nella modalità avanzata.',
pt: 'Esta modalidade simplifica a gestão de cores: você informa apenas as cores principais (primária, secundária, destaque).\n\nAs outras cores são calculadas automaticamente seguindo regras de contraste para se manterem legíveis sobre os fundos.\n\n**Atenção**: ativar este modo redefine quaisquer cores detalhadas definidas manualmente no modo avançado.',
de: 'Dieser Modus vereinfacht die Farbverwaltung: Sie geben nur die Hauptfarben an (Primär-, Sekundär- und Akzentfarbe).\n\nDie übrigen Farben werden automatisch nach Kontrastregeln berechnet, damit sie auf den Hintergründen lesbar bleiben.\n\n**Achtung**: Das Aktivieren dieses Modus setzt alle im erweiterten Modus manuell festgelegten Detailfarben zurück.'
fr: 'Ce mode permet de simplifier la gestion des couleurs. Avec, vous ne renseignez que les couleurs principales (primaire, secondaire, accentuée).\n\nLes autres couleurs sont calculées automatiquement en suivant des règles de contraste pour rester lisibles sur les fonds.\n\n**Attention** : activer ce mode réinitialise les couleurs avancées définies manuellement.',
en: 'This mode simplifies color management: you only provide the main colors (primary, secondary, accent).\n\nThe other colors are computed automatically using contrast rules so they remain legible on backgrounds.\n\n**Warning**: enabling this mode resets the advanced colors set manually.',
es: 'Este modo simplifica la gestión de colores: solo indicas los colores principales (primario, secundario, acentuado).\n\nLos demás colores se calculan automáticamente siguiendo reglas de contraste para que sigan siendo legibles sobre los fondos.\n\n**Atención**: activar este modo restablece los colores avanzados definidos manualmente.',
it: 'Questa modalità semplifica la gestione dei colori: fornisci solo i colori principali (primario, secondario, accento).\n\nGli altri colori vengono calcolati automaticamente seguendo regole di contrasto per restare leggibili sugli sfondi.\n\n**Attenzione**: abilitando questa modalità vengono reimpostati i colori avanzati definiti manualmente.',
pt: 'Esta modalidade simplifica a gestão de cores: você informa apenas as cores principais (primária, secundária, destaque).\n\nAs outras cores são calculadas automaticamente seguindo regras de contraste para se manterem legíveis sobre os fundos.\n\n**Atenção**: ativar este modo redefine as cores avançadas definidas manualmente.',
de: 'Dieser Modus vereinfacht die Farbverwaltung: Sie geben nur die Hauptfarben an (Primär-, Sekundär- und Akzentfarbe).\n\nDie übrigen Farben werden automatisch nach Kontrastregeln berechnet, damit sie auf den Hintergründen lesbar bleiben.\n\n**Achtung**: Das Aktivieren dieses Modus setzt die manuell festgelegten erweiterten Farben zurück.'
},
default: true
},
Expand Down
34 changes: 29 additions & 5 deletions packages/express/serve-spa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ export function getCSPHeader (cspHeader: CSPHeader, nonce?: boolean) {
else if (typeof cspHeader === 'object') return getCSPHeaderFromDirectives(cspHeader)
}

type FontPreload = { href: string, type?: string }

// escape values interpolated into HTML attributes — the href comes from
// site-configured theme data, so it must not be able to break out of the attribute
const escapeHtmlAttr = (value: string): string =>
value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')

function buildFontPreloadLinks (fonts?: FontPreload[]): string {
return (fonts ?? []).map(f => {
const type = f.type ? ` type="${escapeHtmlAttr(f.type)}"` : ''
return `<link rel="preload" as="font"${type} href="${escapeHtmlAttr(f.href)}" crossorigin>`
}).join('')
}

function insertFontPreloads (html: string, fontLinks: string): string {
if (!fontLinks) return html
const themeIdx = html.indexOf('_theme.css')
if (themeIdx === -1) return html
const linkStart = html.lastIndexOf('<link', themeIdx)
if (linkStart === -1) return html
return html.slice(0, linkStart) + fontLinks + html.slice(linkStart)
}

type ServeSpaOptions = {
ignoreSitePath?: boolean,
privateDirectoryUrl?: string,
Expand Down Expand Up @@ -78,8 +101,9 @@ async function createHtmlMiddleware (directory: string, baseParams: Record<strin
html = microTemplate(html, siteExtraParams)
}
if (options?.privateDirectoryUrl) {
const hashes = await getSiteHashes(options.privateDirectoryUrl, siteUrl)
html = microTemplate(html, hashes)
const { fonts, ...templateHashes } = await getSiteHashes(options.privateDirectoryUrl, siteUrl)
html = microTemplate(html, templateHashes)
html = insertFontPreloads(html, buildFontPreloadLinks(fonts))
}
}

Expand Down Expand Up @@ -158,7 +182,7 @@ export function prepareUiConfig (uiConfig: any) {
return { uiConfigStr, uiConfigJs, uiConfigPath }
}

type Hashes = { THEME_CSS_HASH: string, PUBLIC_SITE_INFO_HASH: string }
type Hashes = { THEME_CSS_HASH: string, PUBLIC_SITE_INFO_HASH: string, fonts: FontPreload[] }
const cache: Record<string, { hashes: Promise<Hashes>, ts: number }> = {}

const minuteMS = 1000 * 60
Expand All @@ -167,13 +191,13 @@ const getSiteHashes = async (privateDirectoryUrl: string, siteUrl: string) => {
const now = new Date().getTime()
if (!cache[siteUrl] || cache[siteUrl].ts < (now - minuteMS)) {
const url = new URL(siteUrl)
const hashes = axios.get<{ publicInfo: string, themeCss: string }>(privateDirectoryUrl + '/simple-directory/api/sites/_hashes', {
const hashes = axios.get<{ publicInfo: string, themeCss: string, fonts?: FontPreload[] }>(privateDirectoryUrl + '/simple-directory/api/sites/_hashes', {
headers: {
'x-forwarded-proto': url.protocol.slice(0, -1),
'x-forwarded-host': url.hostname,
'x-forwarded-port': url.port
}
}).then(r => ({ THEME_CSS_HASH: r.data.themeCss + '/', PUBLIC_SITE_INFO_HASH: r.data.publicInfo + '/' }))
}).then(r => ({ THEME_CSS_HASH: r.data.themeCss + '/', PUBLIC_SITE_INFO_HASH: r.data.publicInfo + '/', fonts: r.data.fonts ?? [] }))
cache[siteUrl] = { ts: now, hashes }
}
return cache[siteUrl].hashes
Expand Down