SEO : comble les régressions de la migration Astro → SvelteKit

- composant Seo.svelte partagé : canonical, Open Graph complet
  (type, url, title, description, image, locale fr_FR, site_name)
  et Twitter Cards sur toutes les pages
- branché sur l'accueil, /outils, /quiz, /contribuer et les fiches
  outils (les meta OG en dur des fiches sont remplacés par le
  composant, JSON-LD conservé)
- script prebuild gen-sitemap.mjs : régénère static/sitemap.xml
  (59 URLs, slugs relus depuis content/outils/) — remplace le
  sitemap qu'apportait @astrojs/sitemap
- robots.txt référence le sitemap en URL absolue
This commit is contained in:
sucupira
2026-07-21 13:50:03 -04:00
parent 3e60f48c5e
commit 848e33d198
11 changed files with 405 additions and 21 deletions
+43
View File
@@ -0,0 +1,43 @@
<script lang="ts">
import { t } from '$lib/i18n/index.svelte'
import { SITE_URL } from '$lib/site'
interface Props {
/** titre court de la page (« Annuaire des outils ») — omis sur l'accueil */
title?: string
description: string
/** chemin canonique avec slash final, ex. `/outils/` (trailingSlash: 'always') */
path: string
type?: 'website' | 'article'
}
let { title, description, path, type = 'website' }: Props = $props()
// même format que l'ère actuelle : « Titre — Lagé Chat Control »,
// et « Lagé Chat Control — accroche » sur l'accueil
let fullTitle = $derived(title ? `${title}${t('site.name')}` : `${t('site.name')}${t('site.tagline')}`)
let url = $derived(`${SITE_URL}${path}`)
// favicon.png servi en og:image — réellement 192×192 (vérifié au build)
const image = `${SITE_URL}/favicon.png`
</script>
<svelte:head>
<title>{fullTitle}</title>
<meta name="description" content={description} />
<link rel="canonical" href={url} />
<meta property="og:type" content={type} />
<meta property="og:url" content={url} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:image:width" content="192" />
<meta property="og:image:height" content="192" />
<meta property="og:image:alt" content={t('site.name')} />
<meta property="og:locale" content="fr_FR" />
<meta property="og:site_name" content={t('site.name')} />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
</svelte:head>
+3
View File
@@ -1,5 +1,8 @@
/** Constantes du site — un seul endroit à changer si la forge bouge. */
export const REPO_URL = 'https://labola.o-k-i.net/cyber-mawonaj/lage-chat-control'
/** Origine de production — base des URL absolues SEO (canonical, OG, sitemap). */
export const SITE_URL = 'https://chatcontrol.o-k-i.net'
export const NEW_TOOL_ISSUE_URL = `${REPO_URL}/issues/new?template=.gitea/ISSUE_TEMPLATE/nouvo-zouti.yaml`
/**
+2 -4
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { base } from '$app/paths'
import Seo from '$lib/components/Seo.svelte'
import ToolCard from '$lib/components/ToolCard.svelte'
import { adoptedCount } from '$lib/adopted.svelte'
import { t } from '$lib/i18n/index.svelte'
@@ -10,10 +11,7 @@
let jes = $derived(adoptedCount())
</script>
<svelte:head>
<title>{t('site.name')}{t('site.tagline')}</title>
<meta name="description" content={t('site.description')} />
</svelte:head>
<Seo description={t('site.description')} path="/" />
<!-- Valeur livrée en < 5 s (doctrine §2.1) : le héros dit quoi, pour qui,
et donne UNE action primaire (Von Restorff, doctrine §1.2). -->
+2 -4
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import Seo from '$lib/components/Seo.svelte'
import { t } from '$lib/i18n/index.svelte'
import { NEW_TOOL_ISSUE_URL, REPO_URL } from '$lib/site'
@@ -10,10 +11,7 @@
] as const
</script>
<svelte:head>
<title>{t('contrib.title')}{t('site.name')}</title>
<meta name="description" content={t('contrib.intro')} />
</svelte:head>
<Seo title={t('contrib.title')} description={t('contrib.intro')} path="/contribuer/" />
<header class="stack">
<h1>{t('contrib.title')}</h1>
+2 -4
View File
@@ -7,6 +7,7 @@
import ToolCard from '$lib/components/ToolCard.svelte'
import EndMarker from '$lib/components/EndMarker.svelte'
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
import Seo from '$lib/components/Seo.svelte'
import { t } from '$lib/i18n/index.svelte'
let { data } = $props()
@@ -43,10 +44,7 @@
}
</script>
<svelte:head>
<title>{t('directory.title')}{t('site.name')}</title>
<meta name="description" content={t('directory.intro')} />
</svelte:head>
<Seo title={t('directory.title')} description={t('directory.intro')} path="/outils/" />
<header class="stack">
<h1>{t('directory.title')}</h1>
@@ -2,6 +2,7 @@
import { base } from '$app/paths'
import AdoptButton from '$lib/components/AdoptButton.svelte'
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
import Seo from '$lib/components/Seo.svelte'
import { getLocale, t } from '$lib/i18n/index.svelte'
let { data } = $props()
@@ -34,15 +35,12 @@
</script>
<svelte:head>
<title>{tool.name}{t('site.name')}</title>
<meta name="description" content={tool.excerpt} />
<meta property="og:title" content={tool.name} />
<meta property="og:description" content={tool.excerpt} />
<meta property="og:type" content="article" />
<!-- eslint-disable-next-line svelte/no-at-html-tags — JSON-LD généré au build depuis nos propres données -->
{@html jsonLd}
</svelte:head>
<Seo title={tool.name} description={tool.excerpt} path="/outils/{tool.slug}/" type="article" />
<nav aria-label={t('tool.backToDirectory')}>
<a href="{base}/outils">{t('tool.backToDirectory')}</a>
</nav>
+2 -4
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { base } from '$app/paths'
import Seo from '$lib/components/Seo.svelte'
import { t } from '$lib/i18n/index.svelte'
import type { QuizContent } from '$lib/content/quiz.server'
@@ -108,10 +109,7 @@
}
</script>
<svelte:head>
<title>{t('quiz.title')} — {t('site.name')}</title>
<meta name="description" content={t('quiz.lede')} />
</svelte:head>
<Seo title={t('quiz.title')} description={t('quiz.lede')} path="/quiz/" />
<header class="stack">
<p class="eyebrow">{t('quiz.eyebrow')}</p>