feat: support du déploiement sous sous-chemin (paths.base)
- Liens et assets via {base}, polices déplacées vers src/lib/assets (hachage Vite)
- Service worker base-aware, précache sur les pages prérendues
- CSP hash documentée : l'hôte ne doit pas fixer script-src
- Docs : déploiement Apache/YunoHost (DEPLOYMENT, APACHE_AUTOINDEX)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { base } from '$app/paths'
|
||||
import { page } from '$app/state'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
@@ -19,7 +20,7 @@
|
||||
let current = $derived(browser ? page.url.searchParams.get('cat') : null)
|
||||
|
||||
function href(slug: string | null): string {
|
||||
return slug ? `/outils?cat=${slug}` : '/outils'
|
||||
return slug ? `${base}/outils?cat=${slug}` : `${base}/outils`
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { base } from '$app/paths'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
// Le clavier est roi sur desktop (doctrine §2.2.3) : raccourcis découvrables
|
||||
@@ -23,9 +24,9 @@
|
||||
e.preventDefault()
|
||||
dialog.open ? dialog.close() : dialog.showModal()
|
||||
} else if (e.key === 'h') {
|
||||
goto('/')
|
||||
goto(`${base}/`)
|
||||
} else if (e.key === 'a') {
|
||||
goto('/outils')
|
||||
goto(`${base}/outils`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths'
|
||||
import DifficultyBadge from './DifficultyBadge.svelte'
|
||||
import { isAdopted } from '$lib/adopted.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
@@ -22,7 +23,7 @@
|
||||
<span class="monogram" style:--tool-color={tool.color} aria-hidden="true">
|
||||
{tool.monogram ?? tool.name.slice(0, 2)}
|
||||
</span>
|
||||
<h3><a href="/outils/{tool.slug}">{tool.name}</a></h3>
|
||||
<h3><a href="{base}/outils/{tool.slug}">{tool.name}</a></h3>
|
||||
{#if isAdopted(tool.slug)}
|
||||
<!-- information périphérique (doctrine §3.5) : discret, jamais bloquant -->
|
||||
<span class="adopted" title={t('adopted.marked')}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import '../app.css'
|
||||
import { onNavigate } from '$app/navigation'
|
||||
import { base } from '$app/paths'
|
||||
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte'
|
||||
import OfflineBadge from '$lib/components/OfflineBadge.svelte'
|
||||
import ShortcutsDialog from '$lib/components/ShortcutsDialog.svelte'
|
||||
@@ -28,15 +29,15 @@
|
||||
|
||||
<header class="center">
|
||||
<div class="cluster bar">
|
||||
<a class="brand" href="/">
|
||||
<a class="brand" href="{base}/">
|
||||
<span class="brand-mark" aria-hidden="true"></span>
|
||||
{t('site.name')}
|
||||
</a>
|
||||
<nav aria-label={t('nav.home')}>
|
||||
<ul class="cluster">
|
||||
<li><a href="/outils">{t('nav.directory')}</a></li>
|
||||
<li><a href="/quiz">{t('quiz.nav')}</a></li>
|
||||
<li><a href="/contribuer">{t('nav.contribute')}</a></li>
|
||||
<li><a href="{base}/outils">{t('nav.directory')}</a></li>
|
||||
<li><a href="{base}/quiz">{t('quiz.nav')}</a></li>
|
||||
<li><a href="{base}/contribuer">{t('nav.contribute')}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<LanguageSwitcher />
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
// Tout le site est prérendu (ADR-001) : HTML statique servi par nginx,
|
||||
// un lien fonctionne sans JS (doctrine §4.4).
|
||||
export const prerender = true
|
||||
|
||||
// Chaque page est générée comme `<route>/index.html` (et non `<route>.html`) :
|
||||
// les URL canoniques finissent par `/`, ce que tout serveur statique sert sans
|
||||
// règle spéciale (nginx `try_files $uri/`, Apache mod_dir…) — et ça évite le
|
||||
// conflit fichier/dossier documenté dans docs/APACHE_AUTOINDEX.md.
|
||||
export const trailingSlash = 'always'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths'
|
||||
import ToolCard from '$lib/components/ToolCard.svelte'
|
||||
import { adoptedCount } from '$lib/adopted.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
@@ -19,7 +20,7 @@
|
||||
<section class="hero stack">
|
||||
<h1>{t('hero.title')}</h1>
|
||||
<p class="sub">{t('hero.subtitle')}</p>
|
||||
<p><a class="cta" href="/outils">{t('hero.cta')} · {data.toolCount} {t('directory.results')}</a></p>
|
||||
<p><a class="cta" href="{base}/outils">{t('hero.cta')} · {data.toolCount} {t('directory.results')}</a></p>
|
||||
{#if jes > 0}
|
||||
<p class="jes" role="status">
|
||||
<span class="jes-mark" aria-hidden="true">✓</span>
|
||||
@@ -56,7 +57,7 @@
|
||||
<ul class="cluster">
|
||||
{#each arc.categories as cat (cat.slug)}
|
||||
<li>
|
||||
<a href="/outils?cat={cat.slug}">
|
||||
<a href="{base}/outils?cat={cat.slug}">
|
||||
{cat.title}
|
||||
<span class="count">{cat.count}</span>
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { base } from '$app/paths'
|
||||
import { page } from '$app/state'
|
||||
import CategoryNav from '$lib/components/CategoryNav.svelte'
|
||||
import SearchBar from '$lib/components/SearchBar.svelte'
|
||||
@@ -38,7 +39,7 @@
|
||||
if (d) params.set('diff', d)
|
||||
else params.delete('diff')
|
||||
const qs = params.toString()
|
||||
return qs ? `/outils?${qs}` : '/outils'
|
||||
return qs ? `${base}/outils?${qs}` : `${base}/outils`
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths'
|
||||
import AdoptButton from '$lib/components/AdoptButton.svelte'
|
||||
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
|
||||
import { getLocale, t } from '$lib/i18n/index.svelte'
|
||||
@@ -43,7 +44,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<nav aria-label={t('tool.backToDirectory')}>
|
||||
<a href="/outils">← {t('tool.backToDirectory')}</a>
|
||||
<a href="{base}/outils">← {t('tool.backToDirectory')}</a>
|
||||
</nav>
|
||||
|
||||
<article class="stack" style="--gap: var(--space-4)">
|
||||
@@ -57,7 +58,7 @@
|
||||
<div class="cluster meta">
|
||||
<DifficultyBadge difficulty={tool.difficulty} />
|
||||
{#if data.category}
|
||||
<a href="/outils?cat={data.category.slug}">{data.category.title}</a>
|
||||
<a href="{base}/outils?cat={data.category.slug}">{data.category.title}</a>
|
||||
{/if}
|
||||
{#if tool.license}
|
||||
<span>{t('tool.license')} : {tool.license}</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
import type { QuizContent } from '$lib/content/quiz.server'
|
||||
|
||||
@@ -119,7 +120,7 @@
|
||||
<p class="privacy">{t('quiz.privacyNote')}</p>
|
||||
<p class="cluster">
|
||||
<a class="cta" href="#quiz-form">{t('quiz.start')}</a>
|
||||
<a class="secondary" href="/outils">{t('quiz.readDirectory')}</a>
|
||||
<a class="secondary" href="{base}/outils">{t('quiz.readDirectory')}</a>
|
||||
</p>
|
||||
<noscript>
|
||||
<p class="privacy">{t('quiz.privacyNote')} — JavaScript est requis pour calculer ton score.</p>
|
||||
@@ -196,7 +197,7 @@
|
||||
<li class="rec-card stack">
|
||||
<strong>{r.question.label}</strong>
|
||||
<p class="prose">{r.question.rec}</p>
|
||||
<a href="/outils?cat={r.question.category}">{t('quiz.openDirectory')}</a>
|
||||
<a href="{base}/outils?cat={r.question.category}">{t('quiz.openDirectory')}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -231,7 +232,7 @@
|
||||
<li class="cat-card stack">
|
||||
<strong>{q.label}</strong>
|
||||
<p class="prose">{q.rec}</p>
|
||||
<a href="/outils?cat={q.category}">{t('quiz.openDirectory')}</a>
|
||||
<a href="{base}/outils?cat={q.category}">{t('quiz.openDirectory')}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -8,12 +8,17 @@
|
||||
* - assets versionnés (/_app/immutable/…) : cache-first, immuables ;
|
||||
* - pages HTML : network-first avec repli cache — chaque page visitée
|
||||
* devient disponible hors-ligne (« cache des fiches consultées ») ;
|
||||
* - coquille (/, /outils, polices, manifest) : précachée à l'installation.
|
||||
* - coquille (pages prérendues, assets, manifest) : précachée à l'installation.
|
||||
*/
|
||||
import { build, files, version } from '$service-worker'
|
||||
import { build, files, prerendered, version } from '$service-worker'
|
||||
|
||||
// $app/paths n'est pas importable dans un service worker : la base se déduit
|
||||
// de l'URL du script lui-même, servi sous `${base}/service-worker.js`.
|
||||
const base = new URL('.', self.location.href).pathname.replace(/\/$/, '')
|
||||
|
||||
const CACHE = `oki-${version}`
|
||||
const SHELL = ['/', '/outils', '/quiz', '/contribuer', ...build, ...files]
|
||||
// `prerendered` liste toutes les pages prérendues, préfixe base déjà inclus
|
||||
const SHELL = [...prerendered, ...build, ...files]
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
@@ -40,7 +45,7 @@ self.addEventListener('fetch', (event) => {
|
||||
if (url.origin !== self.location.origin) return
|
||||
|
||||
// assets au nom haché : jamais modifiés, cache-first
|
||||
if (url.pathname.startsWith('/_app/immutable/')) {
|
||||
if (url.pathname.startsWith(`${base}/_app/immutable/`)) {
|
||||
event.respondWith(
|
||||
caches.match(request).then((hit) => hit ?? fetch(request))
|
||||
)
|
||||
@@ -61,7 +66,7 @@ self.addEventListener('fetch', (event) => {
|
||||
// navigation vers une page jamais visitée : renvoyer l'accueil
|
||||
// plutôt qu'une erreur réseau brute
|
||||
if (request.mode === 'navigate') {
|
||||
const home = await cache.match('/')
|
||||
const home = await cache.match(`${base}/`)
|
||||
if (home) return home
|
||||
}
|
||||
throw new Error('offline')
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
* Polices auto-hébergées, servies depuis notre domaine — jamais de Google
|
||||
* Fonts (doctrine §5.3 : performance, RGPD, souveraineté).
|
||||
* Budget : 2 fichiers WOFF2 maximum. Le gras du corps est synthétisé.
|
||||
* Fichiers dans src/lib/assets/fonts : Vite les hache sous _app/immutable,
|
||||
* l'URL reste donc correcte quel que soit paths.base.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Atkinson Hyperlegible';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/atkinson-hyperlegible-latin-400-normal.woff2') format('woff2');
|
||||
src: url('../lib/assets/fonts/atkinson-hyperlegible-latin-400-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -16,5 +18,5 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/fonts/fraunces-latin-700-normal.woff2') format('woff2');
|
||||
src: url('../lib/assets/fonts/fraunces-latin-700-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"short_name": "Lagé CC",
|
||||
"description": "Annuaire d'outils libres pour la souveraineté numérique",
|
||||
"lang": "fr",
|
||||
"start_url": "/",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#17181f",
|
||||
"theme_color": "#17181f",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"src": "favicon.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
|
||||
@@ -10,6 +10,26 @@ const config = {
|
||||
adapter: adapter({
|
||||
fallback: '404.html'
|
||||
}),
|
||||
// Déployé à la racine de https://chatcontrol.o-k-i.net/ (YunoHost) :
|
||||
// pas de paths.base. Pour servir sous un sous-chemin, ajouter ici
|
||||
// paths: { base: '/prefixe' } et adapter les règles du serveur.
|
||||
// Le script de démarrage SvelteKit est inline (il embarque les données de
|
||||
// la page) : une CSP `script-src 'self'` posée en en-tête HTTP le bloque
|
||||
// et tue l'hydratation (recherche et filtres morts — vécu sur o2switch).
|
||||
// En mode hash, chaque page prérendue porte une <meta> CSP avec l'empreinte
|
||||
// de SON script inline. L'en-tête serveur ne doit donc plus fixer
|
||||
// script-src (voir DEPLOYMENT.md §2/§3).
|
||||
csp: {
|
||||
mode: 'hash',
|
||||
directives: {
|
||||
'script-src': ['self']
|
||||
}
|
||||
},
|
||||
// Un seul bundle JS + une seule feuille CSS au lieu de ~30 chunks :
|
||||
// moins de requêtes par page (3G, anti-flood o2switch qui répond 429).
|
||||
output: {
|
||||
bundleStrategy: 'single'
|
||||
},
|
||||
prerender: {
|
||||
handleHttpError: 'fail'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user