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
1 change: 1 addition & 0 deletions packages/common-types/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const defaultTheme = {
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}",
headingFontFamily: undefined,
headingFontFamilyCss: undefined,
preloadLinks: [{ href: '/simple-directory/fonts/XRXV3I6Li01BKofINeaB-BaTF6Vo7.woff2', as: 'font', type: 'font/woff2', crossorigin: true }],
colors: {
// standard vuetify colors, see https://vuetifyjs.com/en/styles/colors/#material-colors
background: '#FAFAFA', // grey-lighten-5
Expand Down
25 changes: 25 additions & 0 deletions packages/common-types/theme/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,31 @@ export default {
},
type: 'string'
},
preloadLinks: {
title: 'Liens de préchargement',
'x-i18n-title': {
fr: 'Liens de préchargement',
en: 'Preload links',
es: 'Enlaces de precarga',
it: 'Collegamenti di precaricamento',
pt: 'Links de pré-carregamento',
de: 'Vorlade-Links'
},
description: 'Liste des ressources (polices, etc.) à précharger via des balises <link rel="preload">. Généralement renseignée automatiquement.',
type: 'array',
items: {
type: 'object',
additionalProperties: true,
required: ['href'],
properties: {
href: { type: 'string', title: 'URL' },
as: { type: 'string', title: 'Type de ressource (attribut as)' },
type: { type: 'string', title: 'Type MIME' },
crossorigin: { type: 'boolean', title: 'Requête CORS (crossorigin)' }
}
},
layout: 'none'
},
assistedMode: {
type: 'boolean',
title: 'Mode de gestion des couleurs simplifié',
Expand Down
28 changes: 23 additions & 5 deletions packages/express/serve-spa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { static as expressStatic, type Request } from 'express'
import { reqSitePath, reqSiteUrl, reqOrigin } from '@data-fair/lib-express'
import serialize from 'serialize-javascript'
import axios from '@data-fair/lib-node/axios.js'
import type { Theme } from '@data-fair/lib-common-types/theme/index.js'

type CSPDirectives = Record<string, string | string[]>
type CSPHeader = string | CSPDirectives | boolean
Expand Down Expand Up @@ -49,6 +50,23 @@ export function getCSPHeader (cspHeader: CSPHeader, nonce?: boolean) {
else if (typeof cspHeader === 'object') return getCSPHeaderFromDirectives(cspHeader)
}

// the _hashes payload returns the theme's preload links verbatim
type PreloadLink = NonNullable<Theme['preloadLinks']>[number]

// 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 buildPreloadLinks (links?: PreloadLink[]): string {
return (links ?? []).map(l => {
const as = l.as ? ` as="${escapeHtmlAttr(l.as)}"` : ''
const type = l.type ? ` type="${escapeHtmlAttr(l.type)}"` : ''
const crossorigin = l.crossorigin ? ' crossorigin' : ''
return `<link rel="preload"${as}${type} href="${escapeHtmlAttr(l.href)}"${crossorigin}>`
}).join('')
}

type ServeSpaOptions = {
ignoreSitePath?: boolean,
privateDirectoryUrl?: string,
Expand Down Expand Up @@ -78,8 +96,8 @@ 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 { preloadLinks, ...templateHashes } = await getSiteHashes(options.privateDirectoryUrl, siteUrl)
html = microTemplate(html, { ...templateHashes, PRELOAD_LINKS: buildPreloadLinks(preloadLinks) })
}
}

Expand Down Expand Up @@ -158,7 +176,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, preloadLinks: PreloadLink[] }
const cache: Record<string, { hashes: Promise<Hashes>, ts: number }> = {}

const minuteMS = 1000 * 60
Expand All @@ -167,13 +185,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, preloadLinks?: PreloadLink[] }>(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 + '/', preloadLinks: r.data.preloadLinks ?? [] }))
cache[siteUrl] = { ts: now, hashes }
}
return cache[siteUrl].hashes
Expand Down