diff --git a/docs/DEPLOIEMENT-SVELTEKIT.md b/docs/DEPLOIEMENT-SVELTEKIT.md
index 5140d52..614eca5 100644
--- a/docs/DEPLOIEMENT-SVELTEKIT.md
+++ b/docs/DEPLOIEMENT-SVELTEKIT.md
@@ -42,6 +42,10 @@ Exemple de cron quotidien (sur une machine de build) :
- `manifest.webmanifest` + service worker Workbox (`vite-plugin-pwa`, generateSW) : précache du shell, `navigateFallback: /offline/index.html`.
- Enregistrement par `static/registerSW.js` (chemins absolus), jamais par injection du plugin.
+## Configuration
+
+Toute la configuration d'instance vit dans `src/lib/config.ts` (équivalent des anciennes constantes PHP) : instances agrégées, catégories prioritaires, hashtags, hero, dons, bloc « À propos » (`about`), annonce du prochain live (`nextLive`) et **verrou maintenance** (`countdown`). Quand `countdown.enabled` vaut `true`, le build rend la page de compte à rebours multi-fuseaux (titre kréyòl, redirection automatique à l'échéance) à la place de tout le site — l'équivalent statique de l'ancien `COUNTDOWN_ENABLED`.
+
## Vérifications avant mise en production
```bash
diff --git a/src/lib/components/CountdownClock.svelte b/src/lib/components/CountdownClock.svelte
index 279c420..7ee09a9 100644
--- a/src/lib/components/CountdownClock.svelte
+++ b/src/lib/components/CountdownClock.svelte
@@ -2,16 +2,24 @@
import { onMount } from 'svelte';
import type { Locale } from '$lib/types';
- let { target, locale }: { target: string; locale: Locale } = $props();
+ let { target, locale, onReached = undefined }: { target: string; locale: Locale; onReached?: () => void } = $props();
let remaining = $state<{ days: number; hours: number; minutes: number; seconds: number } | null>(
null
);
+ let reached = false;
function compute(): typeof remaining {
const targetTime = new Date(target.replace(' ', 'T')).getTime();
const diff = targetTime - Date.now();
- if (Number.isNaN(targetTime) || diff <= 0) return null;
+ if (Number.isNaN(targetTime)) return null;
+ if (diff <= 0) {
+ if (!reached) {
+ reached = true;
+ onReached?.();
+ }
+ return null;
+ }
return {
days: Math.floor(diff / 86_400_000),
hours: Math.floor((diff % 86_400_000) / 3_600_000),
diff --git a/src/lib/components/CountdownLock.svelte b/src/lib/components/CountdownLock.svelte
new file mode 100644
index 0000000..d087455
--- /dev/null
+++ b/src/lib/components/CountdownLock.svelte
@@ -0,0 +1,99 @@
+
+
+
+
+
+ {t.countdown.title}
+ {t.countdown.text}
+
+
+
+ {t.direct.inTimezones}
+
+ {#each lock.schedule as slot (slot.label)}
+ -
+ {slot.label}
+ {slot.time}
+
+ {/each}
+
+
+ {t.countdown.back}
+
+
+
+
diff --git a/src/lib/components/OfflineIndicator.svelte b/src/lib/components/OfflineIndicator.svelte
new file mode 100644
index 0000000..7ff4caa
--- /dev/null
+++ b/src/lib/components/OfflineIndicator.svelte
@@ -0,0 +1,50 @@
+
+
+{#if offline}
+
+
+ {label}
+
+{/if}
+
+
diff --git a/src/lib/config.ts b/src/lib/config.ts
index cc2e869..41e9a96 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -164,6 +164,28 @@ export const importantTags = ['ANNUKUTECED', 'podcast', 'fediverse', 'audio', 'v
export const popularTags = ['podcast', 'annukuteced', 'fediverse', 'audio'] as const;
+export const about = {
+ enabled: false,
+ title: { fr: 'À propos', en: 'About' } as Record<'fr' | 'en', string>,
+ description: { fr: '', en: '' } as Record<'fr' | 'en', string>,
+ description2: { fr: '', en: '' } as Record<'fr' | 'en', string>,
+ image: '/images/movement_presentation.png',
+ imageAlt: { fr: '', en: '' } as Record<'fr' | 'en', string>,
+ imageCaption: { fr: '', en: '' } as Record<'fr' | 'en', string>
+};
+
+export const countdown = {
+ enabled: false,
+ targetDate: '2025-10-11 00:00:00',
+ timezones: {
+ "Ma'ohi Nui": 'Pacific/Tahiti',
+ 'Martinique / Guadeloupe': 'America/Martinique',
+ Guyane: 'America/Cayenne',
+ France: 'Europe/Paris',
+ Kanaky: 'Pacific/Noumea'
+ } as Record
+};
+
export const instances = [
{
name: 'GADE',
diff --git a/src/lib/i18n/en.json b/src/lib/i18n/en.json
index 312a92d..1beddde 100644
--- a/src/lib/i18n/en.json
+++ b/src/lib/i18n/en.json
@@ -149,6 +149,11 @@
"text": "You are offline. Pages you already visited remain available.",
"cta": "Back to home"
},
+ "countdown": {
+ "title": "An nou tann !",
+ "text": "The site is under maintenance or getting ready for launch. See you at the end of the countdown.",
+ "back": "You will be automatically redirected to the home page."
+ },
"notFound": {
"title": "Paj la pa la",
"text": "This page does not exist or has been moved.",
diff --git a/src/lib/i18n/fr.json b/src/lib/i18n/fr.json
index 0685930..c116e08 100644
--- a/src/lib/i18n/fr.json
+++ b/src/lib/i18n/fr.json
@@ -149,6 +149,11 @@
"text": "Vous êtes hors ligne. Les pages déjà visitées restent disponibles.",
"cta": "Retour à l'accueil"
},
+ "countdown": {
+ "title": "An nou tann !",
+ "text": "Le site est en maintenance ou se prépare au lancement. Rendez-vous à la fin du compte à rebours.",
+ "back": "Vous serez redirigé automatiquement vers l'accueil."
+ },
"notFound": {
"title": "Paj la pa la",
"text": "Cette page n'existe pas ou a été déplacée.",
diff --git a/src/routes/[[locale=locale]]/+layout.svelte b/src/routes/[[locale=locale]]/+layout.svelte
index a683001..fe65594 100644
--- a/src/routes/[[locale=locale]]/+layout.svelte
+++ b/src/routes/[[locale=locale]]/+layout.svelte
@@ -4,6 +4,8 @@
import { onNavigate } from '$app/navigation';
import Footer from '$lib/components/Footer.svelte';
import Nav from '$lib/components/Nav.svelte';
+ import CountdownLock from '$lib/components/CountdownLock.svelte';
+ import OfflineIndicator from '$lib/components/OfflineIndicator.svelte';
import Sidebar from '$lib/components/Sidebar.svelte';
import ScrollProgressBar from '$lib/components/motion/ScrollProgressBar.svelte';
import { prefersReducedMotion } from '$lib/motion/tokens';
@@ -29,9 +31,15 @@
-{data.t.nav.skipToContent}
+{#if !data.lock}
+ {data.t.nav.skipToContent}
+{/if}
+
+{#if data.lock}
+
+{:else}
+{/if}
diff --git a/src/routes/[[locale=locale]]/categories/[id]/+page.svelte b/src/routes/[[locale=locale]]/categories/[id]/+page.svelte
index 1d73602..561d51f 100644
--- a/src/routes/[[locale=locale]]/categories/[id]/+page.svelte
+++ b/src/routes/[[locale=locale]]/categories/[id]/+page.svelte
@@ -4,12 +4,42 @@
import FlagChip from '$lib/components/motion/FlagChip.svelte';
import KineticText from '$lib/components/motion/KineticText.svelte';
import { hrefFor, t as format } from '$lib/i18n';
+ import { site } from '$lib/config';
import { reveal } from '$lib/motion/reveal';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
const { locale, t } = $derived(data);
+
+ const jsonLd = $derived([
+ {
+ '@context': 'https://schema.org',
+ '@type': 'CollectionPage',
+ name: format(t.meta.categoryTitle, { name: data.name }),
+ url: `${site.baseUrl}${hrefFor(locale, `/categories/${data.id}`)}`,
+ isPartOf: { '@type': 'WebSite', name: site.name, url: `${site.baseUrl}/` },
+ inLanguage: locale === 'en' ? 'en' : 'fr'
+ },
+ {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ itemListElement: [
+ {
+ '@type': 'ListItem',
+ position: 1,
+ name: t.nav.home,
+ item: `${site.baseUrl}${hrefFor(locale, '/')}`
+ },
+ {
+ '@type': 'ListItem',
+ position: 2,
+ name: data.name,
+ item: `${site.baseUrl}${hrefFor(locale, `/categories/${data.id}`)}`
+ }
+ ]
+ }
+ ]);
diff --git a/src/routes/[[locale=locale]]/video/[uuid]/+page.svelte b/src/routes/[[locale=locale]]/video/[uuid]/+page.svelte
index 0a66547..0a13521 100644
--- a/src/routes/[[locale=locale]]/video/[uuid]/+page.svelte
+++ b/src/routes/[[locale=locale]]/video/[uuid]/+page.svelte
@@ -71,20 +71,50 @@
}
]);
- const jsonLd = $derived({
- '@context': 'https://schema.org',
- '@type': 'VideoObject',
- name: video.title,
- description: video.description.slice(0, 300),
- thumbnailUrl: video.thumbnail,
- uploadDate: video.date,
- duration: `PT${Math.floor(video.duration / 60)}M${Math.floor(video.duration % 60)}S`,
- embedUrl: video.embedUrl,
- url: pageUrl,
- ...(peertube.showVideoViews
- ? { interactionStatistic: { '@type': 'InteractionCounter', interactionType: { '@type': 'WatchAction' }, userInteractionCount: video.views } }
- : {})
- });
+ const jsonLd = $derived([
+ {
+ '@context': 'https://schema.org',
+ '@type': 'VideoObject',
+ name: video.title,
+ description: video.description.slice(0, 300),
+ thumbnailUrl: video.thumbnail,
+ uploadDate: video.date,
+ duration: `PT${Math.floor(video.duration / 60)}M${Math.floor(video.duration % 60)}S`,
+ embedUrl: video.embedUrl,
+ url: pageUrl,
+ ...(peertube.showVideoViews
+ ? { interactionStatistic: { '@type': 'InteractionCounter', interactionType: { '@type': 'WatchAction' }, userInteractionCount: video.views } }
+ : {})
+ },
+ {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ itemListElement: [
+ {
+ '@type': 'ListItem',
+ position: 1,
+ name: t.nav.home,
+ item: `${site.baseUrl}${hrefFor(locale, '/')}`
+ },
+ ...(data.categoryName && video.category
+ ? [
+ {
+ '@type': 'ListItem',
+ position: 2,
+ name: data.categoryName,
+ item: `${site.baseUrl}${hrefFor(locale, `/categories/${video.category}`)}`
+ }
+ ]
+ : []),
+ {
+ '@type': 'ListItem',
+ position: data.categoryName ? 3 : 2,
+ name: video.title,
+ item: pageUrl
+ }
+ ]
+ }
+ ]);
+
+
+
+
+ #0D0D0D
+
+
+