forked from ORGANISATION-KA-INTERNATIONALE/FEDIVERSE-OKI
Complète le portage des fonctionnalités du README
- Bloc « À propos » configurable (titre, 2 paragraphes, image légendée) + aside hashtags, masqué tant que non défini (ex-MOVEMENT_*) - JSON-LD CollectionPage (catégories) et BreadcrumbList (vidéo, catégorie) - Indicateur visuel de perte de connexion (online/offline) - Verrou maintenance par compte à rebours multi-fuseaux (ex-COUNTDOWN_*) : build-time, titre kréyòl, redirection automatique à l'échéance - browserconfig.xml (tuiles Windows)
This commit is contained in:
@@ -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`.
|
- `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.
|
- 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
|
## Vérifications avant mise en production
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -2,16 +2,24 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { Locale } from '$lib/types';
|
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>(
|
let remaining = $state<{ days: number; hours: number; minutes: number; seconds: number } | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
let reached = false;
|
||||||
|
|
||||||
function compute(): typeof remaining {
|
function compute(): typeof remaining {
|
||||||
const targetTime = new Date(target.replace(' ', 'T')).getTime();
|
const targetTime = new Date(target.replace(' ', 'T')).getTime();
|
||||||
const diff = targetTime - Date.now();
|
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 {
|
return {
|
||||||
days: Math.floor(diff / 86_400_000),
|
days: Math.floor(diff / 86_400_000),
|
||||||
hours: Math.floor((diff % 86_400_000) / 3_600_000),
|
hours: Math.floor((diff % 86_400_000) / 3_600_000),
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CountdownClock from './CountdownClock.svelte';
|
||||||
|
import Icon from './Icon.svelte';
|
||||||
|
import type { Bundle } from '$lib/i18n';
|
||||||
|
import type { Locale } from '$lib/types';
|
||||||
|
|
||||||
|
let {
|
||||||
|
lock,
|
||||||
|
locale,
|
||||||
|
t
|
||||||
|
}: {
|
||||||
|
lock: { targetDate: string; schedule: { label: string; time: string }[] };
|
||||||
|
locale: Locale;
|
||||||
|
t: Bundle;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function onReached() {
|
||||||
|
window.location.assign('/');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flag-bar" aria-hidden="true"></div>
|
||||||
|
<main class="lock">
|
||||||
|
<Icon name="ka" class="lock-icon" />
|
||||||
|
<h1 class="lock-title">{t.countdown.title}</h1>
|
||||||
|
<p class="text-muted">{t.countdown.text}</p>
|
||||||
|
|
||||||
|
<CountdownClock target={lock.targetDate} {locale} {onReached} />
|
||||||
|
|
||||||
|
<h2 class="schedule-heading">{t.direct.inTimezones}</h2>
|
||||||
|
<ul class="schedule">
|
||||||
|
{#each lock.schedule as slot (slot.label)}
|
||||||
|
<li>
|
||||||
|
<span class="zone">{slot.label}</span>
|
||||||
|
<span>{slot.time}</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="text-muted small">{t.countdown.back}</p>
|
||||||
|
</main>
|
||||||
|
<div class="flag-bar" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lock {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-5) var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock :global(.lock-icon) {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
font-size: clamp(1.6rem, 5vw, 2.8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-heading {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule li {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone {
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 12ch;
|
||||||
|
color: var(--accent);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import Icon from './Icon.svelte';
|
||||||
|
|
||||||
|
let { label }: { label: string } = $props();
|
||||||
|
|
||||||
|
let offline = $state(false);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const update = () => (offline = !navigator.onLine);
|
||||||
|
update();
|
||||||
|
window.addEventListener('online', update);
|
||||||
|
window.addEventListener('offline', update);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('online', update);
|
||||||
|
window.removeEventListener('offline', update);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if offline}
|
||||||
|
<div class="offline-banner" role="status">
|
||||||
|
<Icon name="ka" inline />
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.offline-banner {
|
||||||
|
position: fixed;
|
||||||
|
bottom: var(--space-2);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 300;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: 0.5rem var(--space-2);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--rouge-oki);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline-banner :global(.icon) {
|
||||||
|
color: var(--rouge-oki);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -164,6 +164,28 @@ export const importantTags = ['ANNUKUTECED', 'podcast', 'fediverse', 'audio', 'v
|
|||||||
|
|
||||||
export const popularTags = ['podcast', 'annukuteced', 'fediverse', 'audio'] as const;
|
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<string, string>
|
||||||
|
};
|
||||||
|
|
||||||
export const instances = [
|
export const instances = [
|
||||||
{
|
{
|
||||||
name: 'GADE',
|
name: 'GADE',
|
||||||
|
|||||||
@@ -149,6 +149,11 @@
|
|||||||
"text": "You are offline. Pages you already visited remain available.",
|
"text": "You are offline. Pages you already visited remain available.",
|
||||||
"cta": "Back to home"
|
"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": {
|
"notFound": {
|
||||||
"title": "Paj la pa la",
|
"title": "Paj la pa la",
|
||||||
"text": "This page does not exist or has been moved.",
|
"text": "This page does not exist or has been moved.",
|
||||||
|
|||||||
@@ -149,6 +149,11 @@
|
|||||||
"text": "Vous êtes hors ligne. Les pages déjà visitées restent disponibles.",
|
"text": "Vous êtes hors ligne. Les pages déjà visitées restent disponibles.",
|
||||||
"cta": "Retour à l'accueil"
|
"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": {
|
"notFound": {
|
||||||
"title": "Paj la pa la",
|
"title": "Paj la pa la",
|
||||||
"text": "Cette page n'existe pas ou a été déplacée.",
|
"text": "Cette page n'existe pas ou a été déplacée.",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
import { onNavigate } from '$app/navigation';
|
import { onNavigate } from '$app/navigation';
|
||||||
import Footer from '$lib/components/Footer.svelte';
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
import Nav from '$lib/components/Nav.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 Sidebar from '$lib/components/Sidebar.svelte';
|
||||||
import ScrollProgressBar from '$lib/components/motion/ScrollProgressBar.svelte';
|
import ScrollProgressBar from '$lib/components/motion/ScrollProgressBar.svelte';
|
||||||
import { prefersReducedMotion } from '$lib/motion/tokens';
|
import { prefersReducedMotion } from '$lib/motion/tokens';
|
||||||
@@ -29,9 +31,15 @@
|
|||||||
<meta name="generator" content="SvelteKit" />
|
<meta name="generator" content="SvelteKit" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<a href="#main-content" class="skip-link">{data.t.nav.skipToContent}</a>
|
{#if !data.lock}
|
||||||
|
<a href="#main-content" class="skip-link">{data.t.nav.skipToContent}</a>
|
||||||
|
{/if}
|
||||||
<ScrollProgressBar label={data.t.a11y.progressLabel} />
|
<ScrollProgressBar label={data.t.a11y.progressLabel} />
|
||||||
|
<OfflineIndicator label={data.t.offline.text} />
|
||||||
|
|
||||||
|
{#if data.lock}
|
||||||
|
<CountdownLock lock={data.lock} locale={data.locale} t={data.t} />
|
||||||
|
{:else}
|
||||||
<Nav
|
<Nav
|
||||||
locale={data.locale}
|
locale={data.locale}
|
||||||
t={data.t}
|
t={data.t}
|
||||||
@@ -64,6 +72,7 @@
|
|||||||
<Footer locale={data.locale} t={data.t} />
|
<Footer locale={data.locale} t={data.t} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.shell {
|
.shell {
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
|
import { countdown } from '$lib/config';
|
||||||
import { getBundle } from '$lib/i18n';
|
import { getBundle } from '$lib/i18n';
|
||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from './$types';
|
||||||
|
|
||||||
export const load: LayoutLoad = ({ params }) => {
|
export const load: LayoutLoad = ({ params }) => {
|
||||||
const locale = params.locale === 'en' ? ('en' as const) : ('fr' as const);
|
const locale = params.locale === 'en' ? ('en' as const) : ('fr' as const);
|
||||||
return { locale, t: getBundle(locale) };
|
|
||||||
|
let lock = null;
|
||||||
|
if (countdown.enabled && countdown.targetDate) {
|
||||||
|
const target = new Date(countdown.targetDate.replace(' ', 'T'));
|
||||||
|
lock = {
|
||||||
|
targetDate: countdown.targetDate,
|
||||||
|
schedule: Object.entries(countdown.timezones).map(([label, timeZone]) => ({
|
||||||
|
label,
|
||||||
|
time: new Intl.DateTimeFormat(locale === 'en' ? 'en-GB' : 'fr-FR', {
|
||||||
|
dateStyle: 'full',
|
||||||
|
timeStyle: 'short',
|
||||||
|
timeZone
|
||||||
|
}).format(target)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { locale, t: getBundle(locale), lock };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { castopod, hero, popularTags, site } from '$lib/config';
|
import { castopod, hero, popularTags, site, about } from '$lib/config';
|
||||||
|
import { asset } from '$app/paths';
|
||||||
import AudioPlayer from '$lib/components/AudioPlayer.svelte';
|
import AudioPlayer from '$lib/components/AudioPlayer.svelte';
|
||||||
import HeroSection from '$lib/components/HeroSection.svelte';
|
import HeroSection from '$lib/components/HeroSection.svelte';
|
||||||
import MastodonTimeline from '$lib/components/MastodonTimeline.svelte';
|
import MastodonTimeline from '$lib/components/MastodonTimeline.svelte';
|
||||||
@@ -149,20 +150,49 @@
|
|||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<section class="section" aria-labelledby="tags-heading">
|
<section class="section info-tags" aria-label={t.home.about}>
|
||||||
<div class="section-header" use:reveal={0}>
|
{#if about.enabled && about.description[locale]}
|
||||||
<FlagChip />
|
<article class="info-section" use:reveal={0}>
|
||||||
<KineticText as="h2" id="tags-heading" class="section-title" text={t.home.popularTags} />
|
<div class="section-header">
|
||||||
</div>
|
<FlagChip />
|
||||||
<ul class="tags-list">
|
<KineticText
|
||||||
{#each popularTags as tag (tag)}
|
as="h2"
|
||||||
<li>
|
class="section-title"
|
||||||
<a class="tag" href={hrefFor(locale, `/recherche?q=${encodeURIComponent('#' + tag)}`)}>
|
text={about.title[locale] || t.home.about}
|
||||||
<Icon name="hashtag" inline /> {tag}
|
/>
|
||||||
</a>
|
</div>
|
||||||
</li>
|
<p class="info-description">{about.description[locale]}</p>
|
||||||
{/each}
|
{#if about.description2[locale]}
|
||||||
</ul>
|
<p class="info-description">{about.description2[locale]}</p>
|
||||||
|
{/if}
|
||||||
|
{#if about.imageAlt[locale]}
|
||||||
|
<figure class="movement-figure">
|
||||||
|
<img src={asset(about.image)} alt={about.imageAlt[locale]} class="info-image" loading="lazy" decoding="async" />
|
||||||
|
{#if about.imageCaption[locale]}
|
||||||
|
<figcaption class="movement-caption text-muted">
|
||||||
|
{about.imageCaption[locale]}
|
||||||
|
</figcaption>
|
||||||
|
{/if}
|
||||||
|
</figure>
|
||||||
|
{/if}
|
||||||
|
</article>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<aside class="tags-section-container" aria-labelledby="tags-heading" use:reveal={1}>
|
||||||
|
<div class="section-header">
|
||||||
|
<FlagChip />
|
||||||
|
<KineticText as="h2" id="tags-heading" class="section-title" text={t.home.popularTags} />
|
||||||
|
</div>
|
||||||
|
<ul class="tags-list">
|
||||||
|
{#each popularTags as tag (tag)}
|
||||||
|
<li>
|
||||||
|
<a class="tag" href={hrefFor(locale, `/recherche?q=${encodeURIComponent('#' + tag)}`)}>
|
||||||
|
<Icon name="hashtag" inline /> {tag}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -199,4 +229,37 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-tags {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: var(--space-4);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.info-tags {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-description {
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
max-width: 65ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movement-figure {
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-image {
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: var(--border-card);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movement-caption {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,12 +4,42 @@
|
|||||||
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
import FlagChip from '$lib/components/motion/FlagChip.svelte';
|
||||||
import KineticText from '$lib/components/motion/KineticText.svelte';
|
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||||
import { hrefFor, t as format } from '$lib/i18n';
|
import { hrefFor, t as format } from '$lib/i18n';
|
||||||
|
import { site } from '$lib/config';
|
||||||
import { reveal } from '$lib/motion/reveal';
|
import { reveal } from '$lib/motion/reveal';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
const { locale, t } = $derived(data);
|
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}`)}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Seo
|
<Seo
|
||||||
@@ -17,6 +47,7 @@
|
|||||||
description={format(t.meta.categoryDescription, { name: data.name })}
|
description={format(t.meta.categoryDescription, { name: data.name })}
|
||||||
path={hrefFor(locale, `/categories/${data.id}`)}
|
path={hrefFor(locale, `/categories/${data.id}`)}
|
||||||
{locale}
|
{locale}
|
||||||
|
{jsonLd}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section aria-labelledby="category-heading">
|
<section aria-labelledby="category-heading">
|
||||||
|
|||||||
@@ -71,20 +71,50 @@
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const jsonLd = $derived({
|
const jsonLd = $derived([
|
||||||
'@context': 'https://schema.org',
|
{
|
||||||
'@type': 'VideoObject',
|
'@context': 'https://schema.org',
|
||||||
name: video.title,
|
'@type': 'VideoObject',
|
||||||
description: video.description.slice(0, 300),
|
name: video.title,
|
||||||
thumbnailUrl: video.thumbnail,
|
description: video.description.slice(0, 300),
|
||||||
uploadDate: video.date,
|
thumbnailUrl: video.thumbnail,
|
||||||
duration: `PT${Math.floor(video.duration / 60)}M${Math.floor(video.duration % 60)}S`,
|
uploadDate: video.date,
|
||||||
embedUrl: video.embedUrl,
|
duration: `PT${Math.floor(video.duration / 60)}M${Math.floor(video.duration % 60)}S`,
|
||||||
url: pageUrl,
|
embedUrl: video.embedUrl,
|
||||||
...(peertube.showVideoViews
|
url: pageUrl,
|
||||||
? { interactionStatistic: { '@type': 'InteractionCounter', interactionType: { '@type': 'WatchAction' }, userInteractionCount: video.views } }
|
...(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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Seo
|
<Seo
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/images/android-chrome-192x192.png"/>
|
||||||
|
<TileColor>#0D0D0D</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
Reference in New Issue
Block a user