feat: SEO complet + analytics Umami + og:image § logo
ci/woodpecker/push/woodpecker Pipeline was successful

SEO :
- composable useSeoPage() : og:*, Twitter Cards, canonical sur toutes les pages (15 pages)
- app.vue : JSON-LD Organization + Book, og:image global og-default.png
- og-default.png 1200×630 : logo § calligraphique + texte (Pillow)
- nuxt.config.ts : @nuxtjs/sitemap avec 26 URLs statiques

Analytics Umami :
- useTracking() : helpers typés audio/pdf/player/scroll/cta
- useScrollTracking() : scroll depth 25/50/75/100% + liens externes auto
- useAudioPlayer : trackAudioPlay/Progress/Complete
- BookPdfReader : trackPdfOpen/Close avec durée
- BookPlayer : trackPlayerOpen/Chapter/Mode
- docker-compose : variables NUXT_PUBLIC_UMAMI_* passées au container

Images :
- Couv-Economie-du-don.jpg ajoutée dans public/images/
- bookplayer.config.yml + home.yml : références mises à jour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-04-11 00:25:28 +02:00
parent dcf64cc924
commit 8408fd6466
35 changed files with 723 additions and 44 deletions
+3 -2
View File
@@ -40,8 +40,9 @@ definePageMeta({
layout: 'default',
})
useHead({
title: 'À propos',
useSeoPage({
title: 'À propos — Le Librodrome',
description: 'Le Librodrome : l\'histoire du projet, la démarche et les personnes derrière le livre et les outils d\'autonomie.',
})
const { data: page } = await useAsyncData('about', () =>
+4 -1
View File
@@ -153,7 +153,10 @@ const currentIdx = computed(() => politiqueItems.value.findIndex(i => i.to === c
const prevItem = computed(() => currentIdx.value > 0 ? politiqueItems.value[currentIdx.value - 1] : null)
const nextItem = computed(() => currentIdx.value < politiqueItems.value.length - 1 ? politiqueItems.value[currentIdx.value + 1] : null)
useHead({ title: content.value?.meta?.title ?? slug })
useSeoPage({
title: content.value?.meta?.title ?? slug,
description: content.value?.description as string ?? undefined,
})
</script>
<style scoped>
+2 -1
View File
@@ -409,8 +409,9 @@ const appConfig = useAppConfig()
const decisionUrl = (appConfig.libredecision as { url: string })?.url ?? '#'
const sejeteral0Url = (appConfig.sejeteral0 as { url: string })?.url ?? '#'
useHead({
useSeoPage({
title: content.value?.meta?.title ?? 'Autonomie citoyenne',
description: (content.value as Record<string, unknown> | null)?.description as string ?? 'Décision collective, gouvernance locale, outils citoyens — l\'autonomie politique à l\'échelle des bassins de vie.',
})
</script>
+4 -2
View File
@@ -304,8 +304,10 @@
<script setup lang="ts">
const { data: content } = await usePageContent('economique/commande')
useHead({
title: content.value?.meta?.title ?? 'Commander le livre',
useSeoPage({
title: content.value?.meta?.title ?? 'Commander le livre — Une économie du don',
description: 'Commander l\'édition papier du livre « Une économie du don — enfin concevable » d\'Yvv.',
image: '/images/Couv-Economie-du-don.jpg',
})
</script>
+2 -1
View File
@@ -452,8 +452,9 @@ definePageMeta({
const { data: content } = await usePageContent('economique')
useHead({
useSeoPage({
title: content.value?.meta?.title ?? 'Autonomie économique',
description: (content.value as Record<string, unknown> | null)?.description as string ?? 'Comprendre et expérimenter une économie fondée sur le don, la monnaie libre et la réciprocité.',
})
const showBookPlayer = ref(false)
+5 -2
View File
@@ -59,8 +59,11 @@ if (!chapter.value) {
throw createError({ statusCode: 404, statusMessage: 'Chapitre non trouvé' })
}
useHead({
title: chapter.value?.title,
useSeoPage({
title: chapter.value?.title ?? slug,
description: chapter.value?.description as string ?? `Chapitre « ${chapter.value?.title} » — Une économie du don, enfin concevable.`,
image: '/images/Couv-Economie-du-don.jpg',
type: 'article',
})
// Get adjacent chapters for navigation
+5 -2
View File
@@ -424,8 +424,11 @@ definePageMeta({
const { data: content } = await usePageContent('economique/modele-eco')
useHead({
title: content.value?.meta?.title ?? 'Table des matières',
useSeoPage({
title: content.value?.meta?.title ?? 'Une économie du don — Table des matières',
description: 'Onze chapitres pour explorer les fondements d\'une économie fondée sur le don, la mesure et la monnaie libre.',
image: '/images/Couv-Economie-du-don.jpg',
type: 'book',
})
const { data: chapters } = await useAsyncData('book-toc', () =>
+4 -1
View File
@@ -120,7 +120,10 @@ const currentIdx = computed(() => economieItems.value.findIndex(i => i.to === cu
const prevItem = computed(() => currentIdx.value > 0 ? economieItems.value[currentIdx.value - 1] : null)
const nextItem = computed(() => currentIdx.value < economieItems.value.length - 1 ? economieItems.value[currentIdx.value + 1] : null)
useHead({ title: content.value?.meta?.title ?? 'Monnaie libre' })
useSeoPage({
title: content.value?.meta?.title ?? 'Monnaie libre — Autonomie économique',
description: content.value?.description as string ?? 'La monnaie libre (June / G1) : comprendre la théorie relative de la monnaie et expérimenter une monnaie équitable.',
})
</script>
<style scoped>
@@ -44,8 +44,9 @@
<script setup lang="ts">
const { data: content } = await usePageContent('economique/productions-collectives')
useHead({
useSeoPage({
title: content.value?.meta?.title ?? 'Productions collectives',
description: content.value?.description as string ?? 'Initiatives et productions collectives dans le cadre de l\'économie du don.',
})
</script>
+4 -2
View File
@@ -166,8 +166,10 @@ definePageMeta({
const { data: content } = await usePageContent('en-musique')
const { data: homeContent } = await usePageContent('home')
useHead({
title: content.value?.meta?.title ?? 'En musique',
useSeoPage({
title: content.value?.meta?.title ?? 'En musique — Le Librodrome',
description: 'Neuf chansons qui racontent le livre « Une économie du don ». Écoute libre, paroles et présentation musicale guidée.',
image: '/images/Couv-Economie-du-don.jpg',
})
const store = usePlayerStore()
+3 -2
View File
@@ -440,8 +440,9 @@ definePageMeta({
const { data: content } = await usePageContent('evenement')
const evtContent = computed(() => content.value as Record<string, any> | null)
useHead({
title: evtContent.value?.meta?.title ?? 'Évènement',
useSeoPage({
title: evtContent.value?.meta?.title ?? 'Évènement — Le Librodrome',
description: evtContent.value?.description as string ?? 'Prochains événements du Librodrome : rencontres, lectures et ateliers autour de l\'économie du don.',
})
</script>
+3 -2
View File
@@ -104,8 +104,9 @@
<script setup lang="ts">
const { data: content } = await usePageContent('gratewizard')
useHead({
title: content.value?.meta?.title ?? 'grateWizard \u2014 Coefficients relatifs',
useSeoPage({
title: content.value?.meta?.title ?? 'grateWizard Coefficients relatifs',
description: 'Calculateur de coefficients relatifs pour l\'économie du don et la monnaie libre (DU/June).',
})
const { url, launch } = useGrateWizard()
+3 -2
View File
@@ -9,8 +9,9 @@
</template>
<script setup lang="ts">
useHead({
title: 'Accueil',
useSeoPage({
title: 'Le Librodrome — autonomie numérique, économique, citoyenne',
description: 'Construire une autonomie collective à l\'échelle des bassins de vie. Un livre, des chansons et des outils pour l\'émancipation.',
})
const showBookPlayer = ref(false)
+3 -2
View File
@@ -41,8 +41,9 @@
</template>
<script setup lang="ts">
useHead({
title: 'Messages',
useSeoPage({
title: 'Messages — Le Librodrome',
description: 'Questions, retours et réactions des lecteurs et visiteurs du Librodrome.',
})
const { data: messages } = await useFetch('/api/messages')
+2 -1
View File
@@ -647,8 +647,9 @@ const currentIdx = computed(() => sectionItems.value.findIndex(i => i.to === cur
const prevItem = computed(() => currentIdx.value > 0 ? sectionItems.value[currentIdx.value - 1] : null)
const nextItem = computed(() => currentIdx.value < sectionItems.value.length - 1 ? sectionItems.value[currentIdx.value + 1] : null)
useHead({
useSeoPage({
title: content.value?.meta?.title ?? slug,
description: content.value?.description as string ?? undefined,
})
</script>
+2 -1
View File
@@ -380,8 +380,9 @@ definePageMeta({
const { data: content } = await usePageContent('numerique')
useHead({
useSeoPage({
title: content.value?.meta?.title ?? 'Autonomie numérique',
description: (content.value as Record<string, unknown> | null)?.description as string ?? 'Logiciel libre, authentification Web of Trust, cloud souverain — reprendre le contrôle de ses outils numériques.',
})
</script>