From 8408fd6466080393ec2f59eec607819090013a8f Mon Sep 17 00:00:00 2001 From: Yvv Date: Sat, 11 Apr 2026 00:25:28 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20SEO=20complet=20+=20analytics=20Umami?= =?UTF-8?q?=20+=20og:image=20=C2=A7=20logo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.example | 13 + CLAUDE.md | 46 +++ app/app.vue | 65 +++- app/components/book/BookPdfReader.vue | 10 + app/components/book/BookPlayer.vue | 6 +- app/composables/useAudioPlayer.ts | 20 ++ app/composables/useScrollTracking.ts | 55 ++++ app/composables/useSeoPage.ts | 52 ++++ app/composables/useTracking.ts | 84 +++++- app/layouts/default.vue | 4 + app/pages/a-propos.vue | 5 +- app/pages/citoyenne/[slug].vue | 5 +- app/pages/citoyenne/index.vue | 3 +- app/pages/economique/commande.vue | 6 +- app/pages/economique/index.vue | 3 +- app/pages/economique/modele-eco/[slug].vue | 7 +- app/pages/economique/modele-eco/index.vue | 7 +- app/pages/economique/monnaie-libre.vue | 5 +- .../economique/productions-collectives.vue | 3 +- app/pages/en-musique/index.vue | 6 +- app/pages/evenement.vue | 5 +- app/pages/gratewizard.vue | 5 +- app/pages/index.vue | 5 +- app/pages/messages.vue | 5 +- app/pages/numerique/[slug].vue | 3 +- app/pages/numerique/index.vue | 3 +- docker/docker-compose.umami.yml | 10 +- docker/docker-compose.yml | 3 + nuxt.config.ts | 40 +++ package.json | 1 + pnpm-lock.yaml | 278 ++++++++++++++++++ public/images/Couv-Economie-du-don.jpg | Bin 0 -> 335251 bytes public/og-default.png | Bin 0 -> 47153 bytes site/bookplayer.config.yml | 2 +- site/pages/home.yml | 2 +- 35 files changed, 723 insertions(+), 44 deletions(-) create mode 100644 app/composables/useScrollTracking.ts create mode 100644 app/composables/useSeoPage.ts create mode 100644 public/images/Couv-Economie-du-don.jpg create mode 100644 public/og-default.png diff --git a/.env.example b/.env.example index 440e9e3..ef62cc1 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,16 @@ # Admin authentication NUXT_ADMIN_PASSWORD=changeme NUXT_ADMIN_SECRET=change-this-to-a-random-secret-at-least-32-chars + +# Umami analytics — instance Docker (docker-compose.umami.yml) +# UMAMI_DB_PASSWORD et UMAMI_APP_SECRET : générer avec `openssl rand -hex 32` +UMAMI_DB_PASSWORD= +UMAMI_APP_SECRET= +UMAMI_DOMAIN=stats.librodrome.org + +# Variables injectées dans Nuxt (tracking frontend + API stats) +# NUXT_PUBLIC_UMAMI_WEBSITE_ID : Settings → Websites dans l'interface Umami +# NUXT_UMAMI_API_KEY : Settings → API Keys dans l'interface Umami +NUXT_PUBLIC_UMAMI_URL=https://stats.librodrome.org +NUXT_PUBLIC_UMAMI_WEBSITE_ID= +NUXT_UMAMI_API_KEY= diff --git a/CLAUDE.md b/CLAUDE.md index 2f67dfe..ec7367f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,3 +83,49 @@ PORT=3099 node .output/server/index.mjs # Test build prod (toujours avant commi - Shadoks SVG inline thématiques sur chaque page (hidden mobile, opacity 0.18–0.28) - Hexagramme 益 (#42 Yi, Augmentation) dans `layouts/default.vue` - Signature § (logo calligraphique SVG gradient) dans `TheHeader.vue` — ne pas modifier sans demander + +## SEO & Recherche IA — Checklist permanente + +### À chaque nouvelle page Vue + +1. **Appeler `useSeoPage()`** — jamais `useHead({ title })` seul + ```ts + useSeoPage({ + title: 'Titre page — Le Librodrome', // 50–60 chars + description: 'Description...', // 120–155 chars + image: '/images/og-specifique.jpg', // optionnel, sinon og-default.png (logo §) + type: 'website' | 'article' | 'book', // défaut: website + }) + ``` +2. **og:image** : utiliser la couverture `/images/Couv-Economie-du-don.jpg` pour les pages livre/musique ; `og-default.png` (logo §) pour les pages site générales +3. **type: 'article'** pour les chapitres du modele-eco ; **type: 'book'** pour les index livre + +### À chaque nouveau fichier YAML dans `site/pages/` + +Ajouter le bloc `seo:` en tête : +```yaml +seo: + title: "Titre SEO — 50-60 chars max" + description: "Description 120-155 chars, avec mots-clés naturels" + # image: /images/og-specifique.jpg # optionnel + # keywords: [monnaie libre, TRM, économie du don] +``` + +### Règles description SEO + +- Commence par un verbe d'action ou une affirmation forte +- Contient les mots-clés : économie du don, monnaie libre, TRM, June, bassin de vie, autonomie +- 120–155 caractères (ni trop court, ni tronqué) +- Différente du titre et du H1 + +### JSON-LD automatique + +- JSON-LD `Organization` + `Book` : injecté globalement dans `app.vue` — ne pas redupliquer +- Chapitres → og:type `article` via `useSeoPage({ type: 'article' })` +- Sitemap : géré par `@nuxtjs/sitemap` — ajouter manuellement les nouvelles routes statiques dans `nuxt.config.ts > sitemap.urls` + +### Analytics — Nouveaux composants + +- Tout bouton CTA externe : `trackCta(label, url)` depuis `useTracking()` +- Tout `` : capturé automatiquement par `useScrollTracking()` dans `default.vue` +- Tout nouveau lecteur média : appeler `trackAudioPlay` / `trackPdfOpen` depuis `useTracking()` diff --git a/app/app.vue b/app/app.vue index b3eebd6..69bf52f 100644 --- a/app/app.vue +++ b/app/app.vue @@ -17,24 +17,71 @@ const paletteStore = usePaletteStore() onMounted(() => paletteStore.applyToDOM()) +const config = useRuntimeConfig() +const siteUrl = (config.public.siteUrl as string) || 'https://librodrome.org' + // Umami analytics — inject script only when configured -const runtimeConfig = useRuntimeConfig() -if (runtimeConfig.public.umamiWebsiteId && runtimeConfig.public.umamiUrl) { +if (config.public.umamiWebsiteId && config.public.umamiUrl) { useHead({ script: [{ - src: `${runtimeConfig.public.umamiUrl}/script.js`, + src: `${config.public.umamiUrl}/script.js`, defer: true, - 'data-website-id': runtimeConfig.public.umamiWebsiteId, + 'data-website-id': config.public.umamiWebsiteId, }], }) } +// Global SEO defaults — surchargeables page par page via useSeoPage() useHead({ - titleTemplate: (title) => { - return title ? `${title} — Le Librodrome` : 'Le librodrome' - }, - meta: [ - { name: 'description', content: 'Une économie du don — enfin concevable. Un livre et des chansons, lecture guidée et écoute libre.' }, + titleTemplate: (title) => title ? `${title} — Le Librodrome` : 'Le Librodrome', +}) + +useSeoMeta({ + ogSiteName: 'Le Librodrome', + ogType: 'website', + ogLocale: 'fr_FR', + ogImage: `${siteUrl}/og-default.png`, + ogImageWidth: 1200, + ogImageHeight: 630, + twitterCard: 'summary_large_image', + twitterSite: '@librodrome', +}) + +// JSON-LD — Organisation + Livre +useHead({ + script: [ + { + type: 'application/ld+json', + innerHTML: JSON.stringify({ + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'Organization', + '@id': `${siteUrl}/#organization`, + name: 'Le Librodrome', + url: siteUrl, + logo: { + '@type': 'ImageObject', + url: `${siteUrl}/images/og-default.png`, + }, + description: 'Plateforme d\'autonomie numérique, économique et citoyenne à l\'échelle des bassins de vie.', + }, + { + '@type': 'Book', + '@id': `${siteUrl}/economique/modele-eco#book`, + name: 'Une économie du don — enfin concevable', + author: { '@type': 'Person', name: 'Yvv' }, + publisher: { '@id': `${siteUrl}/#organization` }, + isbn: '979-1-042-45206-3', + inLanguage: 'fr', + image: `${siteUrl}/images/Couv-Economie-du-don.jpg`, + url: `${siteUrl}/economique/modele-eco`, + description: 'Un livre et 9 chansons pour explorer les fondements d\'une économie fondée sur le don.', + license: 'https://creativecommons.org/licenses/by-nc/4.0/', + }, + ], + }), + }, ], }) diff --git a/app/components/book/BookPdfReader.vue b/app/components/book/BookPdfReader.vue index 819023e..bc9537d 100644 --- a/app/components/book/BookPdfReader.vue +++ b/app/components/book/BookPdfReader.vue @@ -40,6 +40,7 @@ const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>() const { data: bpContent } = await usePageContent('book-player') const bookData = useBookData() await bookData.init() +const { trackPdfOpen, trackPdfClose } = useTracking() const overlayRef = ref() const iframeRef = ref() @@ -60,9 +61,18 @@ function close() { isOpen.value = false } +// Tracking state +let pdfOpenedAt = 0 + watch(isOpen, (open) => { if (open) { nextTick(() => overlayRef.value?.focus()) + trackPdfOpen(props.page ? `chapter-p${props.page}` : 'direct') + pdfOpenedAt = Date.now() + } + else if (pdfOpenedAt > 0) { + trackPdfClose(0, Date.now() - pdfOpenedAt) + pdfOpenedAt = 0 } if (import.meta.client) { document.body.style.overflow = open ? 'hidden' : '' diff --git a/app/components/book/BookPlayer.vue b/app/components/book/BookPlayer.vue index 40db53d..6bd34c3 100644 --- a/app/components/book/BookPlayer.vue +++ b/app/components/book/BookPlayer.vue @@ -102,7 +102,7 @@
- +
{{ currentSong.title }} @@ -138,6 +138,7 @@ +