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
+13
View File
@@ -1,3 +1,16 @@
# Admin authentication # Admin authentication
NUXT_ADMIN_PASSWORD=changeme NUXT_ADMIN_PASSWORD=changeme
NUXT_ADMIN_SECRET=change-this-to-a-random-secret-at-least-32-chars 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=
+46
View File
@@ -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.180.28) - Shadoks SVG inline thématiques sur chaque page (hidden mobile, opacity 0.180.28)
- Hexagramme 益 (#42 Yi, Augmentation) dans `layouts/default.vue` - Hexagramme 益 (#42 Yi, Augmentation) dans `layouts/default.vue`
- Signature § (logo calligraphique SVG gradient) dans `TheHeader.vue` — ne pas modifier sans demander - 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', // 5060 chars
description: 'Description...', // 120155 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
- 120155 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 `<a target="_blank">` : capturé automatiquement par `useScrollTracking()` dans `default.vue`
- Tout nouveau lecteur média : appeler `trackAudioPlay` / `trackPdfOpen` depuis `useTracking()`
+56 -9
View File
@@ -17,24 +17,71 @@
const paletteStore = usePaletteStore() const paletteStore = usePaletteStore()
onMounted(() => paletteStore.applyToDOM()) onMounted(() => paletteStore.applyToDOM())
const config = useRuntimeConfig()
const siteUrl = (config.public.siteUrl as string) || 'https://librodrome.org'
// Umami analytics — inject script only when configured // Umami analytics — inject script only when configured
const runtimeConfig = useRuntimeConfig() if (config.public.umamiWebsiteId && config.public.umamiUrl) {
if (runtimeConfig.public.umamiWebsiteId && runtimeConfig.public.umamiUrl) {
useHead({ useHead({
script: [{ script: [{
src: `${runtimeConfig.public.umamiUrl}/script.js`, src: `${config.public.umamiUrl}/script.js`,
defer: true, defer: true,
'data-website-id': runtimeConfig.public.umamiWebsiteId, 'data-website-id': config.public.umamiWebsiteId,
}], }],
}) })
} }
// Global SEO defaults — surchargeables page par page via useSeoPage()
useHead({ useHead({
titleTemplate: (title) => { titleTemplate: (title) => title ? `${title} — Le Librodrome` : 'Le Librodrome',
return title ? `${title} — Le Librodrome` : 'Le librodrome' })
},
meta: [ useSeoMeta({
{ name: 'description', content: 'Une économie du don — enfin concevable. Un livre et des chansons, lecture guidée et écoute libre.' }, 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/',
},
],
}),
},
], ],
}) })
</script> </script>
+10
View File
@@ -40,6 +40,7 @@ const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
const { data: bpContent } = await usePageContent('book-player') const { data: bpContent } = await usePageContent('book-player')
const bookData = useBookData() const bookData = useBookData()
await bookData.init() await bookData.init()
const { trackPdfOpen, trackPdfClose } = useTracking()
const overlayRef = ref<HTMLElement>() const overlayRef = ref<HTMLElement>()
const iframeRef = ref<HTMLIFrameElement>() const iframeRef = ref<HTMLIFrameElement>()
@@ -60,9 +61,18 @@ function close() {
isOpen.value = false isOpen.value = false
} }
// Tracking state
let pdfOpenedAt = 0
watch(isOpen, (open) => { watch(isOpen, (open) => {
if (open) { if (open) {
nextTick(() => overlayRef.value?.focus()) 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) { if (import.meta.client) {
document.body.style.overflow = open ? 'hidden' : '' document.body.style.overflow = open ? 'hidden' : ''
+5 -1
View File
@@ -102,7 +102,7 @@
<!-- Song disc --> <!-- Song disc -->
<div v-if="currentSong" class="reader-song"> <div v-if="currentSong" class="reader-song">
<div class="reader-disc" :class="{ spinning: playerStore.isPlaying }"> <div class="reader-disc" :class="{ spinning: playerStore.isPlaying }">
<img src="/images/book-cover-spread.jpg" alt="" class="reader-disc-img" /> <img src="/images/Couv-Economie-du-don.jpg" alt="" class="reader-disc-img" />
<div class="reader-disc-hole" /> <div class="reader-disc-hole" />
</div> </div>
<span class="reader-song-name">{{ currentSong.title }}</span> <span class="reader-song-name">{{ currentSong.title }}</span>
@@ -138,6 +138,7 @@
<script setup lang="ts"> <script setup lang="ts">
const { data: bpContent } = await usePageContent('book-player') const { data: bpContent } = await usePageContent('book-player')
const { trackPlayerOpen, trackPlayerChapter, trackPlayerMode } = useTracking()
const COL_GAP = 80 const COL_GAP = 80
@@ -167,6 +168,7 @@ const scrollPercent = ref(0)
// When switching back to paginated, recalc pages // When switching back to paginated, recalc pages
watch(readingMode, async (mode) => { watch(readingMode, async (mode) => {
trackPlayerMode(mode === 'scroll' ? 'scroll' : 'guided')
if (mode === 'paginated') { if (mode === 'paginated') {
await nextTick() await nextTick()
await nextTick() await nextTick()
@@ -288,6 +290,7 @@ function goToTrack(idx: number) {
// Play the song // Play the song
const song = tracks.value[idx]?.song const song = tracks.value[idx]?.song
if (song) { if (song) {
trackPlayerChapter(song.id)
_skipSongWatch = true _skipSongWatch = true
audioPlayer.loadAndPlay(song) audioPlayer.loadAndPlay(song)
} }
@@ -378,6 +381,7 @@ function onTouchEnd(e: TouchEvent) {
watch(isOpen, async (open) => { watch(isOpen, async (open) => {
if (open) { if (open) {
showSommaire.value = false showSommaire.value = false
trackPlayerOpen()
await initBookData() await initBookData()
await nextTick() await nextTick()
overlayRef.value?.focus() overlayRef.value?.focus()
+20
View File
@@ -2,9 +2,12 @@ import type { Song } from '~/types/song'
let audio: HTMLAudioElement | null = null let audio: HTMLAudioElement | null = null
let animationFrameId: number | null = null let animationFrameId: number | null = null
// Track which milestones have been fired for the current song
const firedMilestones = new Set<number>()
export function useAudioPlayer() { export function useAudioPlayer() {
const store = usePlayerStore() const store = usePlayerStore()
const { trackAudioPlay, trackAudioComplete, trackAudioProgress } = useTracking()
function getAudio(): HTMLAudioElement { function getAudio(): HTMLAudioElement {
if (!audio) { if (!audio) {
@@ -17,6 +20,10 @@ export function useAudioPlayer() {
}) })
audio.addEventListener('ended', () => { audio.addEventListener('ended', () => {
if (store.currentSong) {
trackAudioComplete(store.currentSong.id, store.currentSong.title)
}
firedMilestones.clear()
const next = store.nextSong() const next = store.nextSong()
if (next) { if (next) {
loadAndPlay(next) loadAndPlay(next)
@@ -36,6 +43,16 @@ export function useAudioPlayer() {
const update = () => { const update = () => {
if (audio && !audio.paused) { if (audio && !audio.paused) {
store.setCurrentTime(audio.currentTime) store.setCurrentTime(audio.currentTime)
// Track progress milestones (25 / 50 / 75 %)
if (audio.duration > 0 && store.currentSong) {
const pct = (audio.currentTime / audio.duration) * 100
for (const milestone of [25, 50, 75] as const) {
if (pct >= milestone && !firedMilestones.has(milestone)) {
firedMilestones.add(milestone)
trackAudioProgress(store.currentSong.id, milestone)
}
}
}
} }
animationFrameId = requestAnimationFrame(update) animationFrameId = requestAnimationFrame(update)
} }
@@ -52,6 +69,7 @@ export function useAudioPlayer() {
async function loadAndPlay(song: Song) { async function loadAndPlay(song: Song) {
const el = getAudio() const el = getAudio()
store.setSong(song) store.setSong(song)
firedMilestones.clear()
// Try OGG first, fall back to MP3 // Try OGG first, fall back to MP3
const oggPath = song.file.replace(/\.mp3$/, '.ogg') const oggPath = song.file.replace(/\.mp3$/, '.ogg')
@@ -64,6 +82,7 @@ export function useAudioPlayer() {
await el.play() await el.play()
store.play() store.play()
startTimeUpdate() startTimeUpdate()
trackAudioPlay(song.id, song.title)
} }
catch { catch {
// If OGG failed, try MP3 // If OGG failed, try MP3
@@ -73,6 +92,7 @@ export function useAudioPlayer() {
await el.play() await el.play()
store.play() store.play()
startTimeUpdate() startTimeUpdate()
trackAudioPlay(song.id, song.title)
} }
catch (err) { catch (err) {
console.error('Playback failed:', err) console.error('Playback failed:', err)
+55
View File
@@ -0,0 +1,55 @@
/**
* Tracks scroll depth milestones (25 / 50 / 75 / 100 %) on each page.
* Also intercepts external link clicks and tracks them.
*
* Usage: call once in default.vue layout.
*/
export function useScrollTracking() {
const route = useRoute()
const { trackScrollDepth, trackExternalLink } = useTracking()
// ── Scroll depth ────────────────────────────────────────────────────────
const fired = new Set<number>()
function onScroll() {
const el = document.documentElement
const scrolled = el.scrollTop + el.clientHeight
const total = el.scrollHeight
if (total <= el.clientHeight) return
const pct = (scrolled / total) * 100
for (const milestone of [25, 50, 75, 100] as const) {
if (pct >= milestone && !fired.has(milestone)) {
fired.add(milestone)
trackScrollDepth(route.path, milestone)
}
}
}
// Reset fired milestones on route change
watch(() => route.path, () => fired.clear())
// ── External link tracking ───────────────────────────────────────────────
function onLinkClick(e: MouseEvent) {
const target = (e.target as HTMLElement).closest('a')
if (!target) return
const href = target.getAttribute('href') || ''
if (!href.startsWith('http') && !href.startsWith('//')) return
try {
const url = new URL(href)
if (url.hostname === 'librodrome.org') return
trackExternalLink(href, target.textContent?.trim() || '')
}
catch { /* invalid URL, ignore */ }
}
onMounted(() => {
window.addEventListener('scroll', onScroll, { passive: true })
document.addEventListener('click', onLinkClick)
})
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
document.removeEventListener('click', onLinkClick)
})
}
+52
View File
@@ -0,0 +1,52 @@
/**
* Applique toutes les balises SEO (og:*, Twitter Cards, canonical, description)
* à partir du contenu YAML d'une page.
*
* Usage dans les pages :
* useSeoPage({ title: content.value?.meta?.title, description: content.value?.description })
*
* L'og:image par défaut est /og-default.png (logo §).
* Chaque section peut surcharger avec son propre image via le champ seo.image du YAML.
*/
export function useSeoPage(opts: {
title?: string | null
description?: string | null
image?: string | null
type?: 'website' | 'article' | 'book'
}) {
const config = useRuntimeConfig()
const route = useRoute()
const siteUrl = (config.public.siteUrl as string) || 'https://librodrome.org'
const title = opts.title || 'Le Librodrome'
const description = opts.description
|| 'Autonomie numérique, économique et citoyenne. Un livre et des chansons sur l\'économie du don.'
const rawImage = opts.image || '/og-default.png'
const image = rawImage.startsWith('http') ? rawImage : `${siteUrl}${rawImage}`
const canonical = `${siteUrl}${route.path}`
const type = opts.type || 'website'
useSeoMeta({
// Open Graph
ogTitle: title,
ogDescription: description,
ogImage: image,
ogImageWidth: 1200,
ogImageHeight: 630,
ogUrl: canonical,
ogType: type,
ogSiteName: 'Le Librodrome',
ogLocale: 'fr_FR',
// Twitter Cards
twitterCard: 'summary_large_image',
twitterTitle: title,
twitterDescription: description,
twitterImage: image,
// Standard
description,
})
useHead({
link: [{ rel: 'canonical', href: canonical }],
})
}
+80 -4
View File
@@ -1,13 +1,19 @@
/** /**
* Umami analytics wrapper — safe server-side, no-op when not configured. * Umami analytics wrapper — safe server-side, no-op when not configured.
* Usage: const { track } = useTracking() *
* track('player:open') * Events convention:
* track('axis:navigate', { axis: 'numerique' }) * audio:play | audio:pause | audio:complete | audio:progress
* pdf:open | pdf:close
* player:open | player:chapter | player:mode
* scroll:depth
* link:external
* cta:click
*/ */
export function useTracking() { export function useTracking() {
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const enabled = !!runtimeConfig.public.umamiWebsiteId const enabled = !!runtimeConfig.public.umamiWebsiteId
// ── Core ────────────────────────────────────────────────────────────────
function track(event: string, data?: Record<string, unknown>) { function track(event: string, data?: Record<string, unknown>) {
if (!import.meta.client || !enabled) return if (!import.meta.client || !enabled) return
const umami = (window as Record<string, unknown>).umami as const umami = (window as Record<string, unknown>).umami as
@@ -16,5 +22,75 @@ export function useTracking() {
umami?.track(event, data) umami?.track(event, data)
} }
return { track, enabled } // ── Audio ────────────────────────────────────────────────────────────────
function trackAudioPlay(songId: string, songTitle: string, context?: string) {
track('audio:play', { song_id: songId, song_title: songTitle, context })
}
function trackAudioPause(songId: string, progressPct: number) {
track('audio:pause', { song_id: songId, progress_pct: progressPct })
}
function trackAudioComplete(songId: string, songTitle: string) {
track('audio:complete', { song_id: songId, song_title: songTitle })
}
/** Fired at 25 / 50 / 75 % milestones */
function trackAudioProgress(songId: string, milestone: 25 | 50 | 75) {
track('audio:progress', { song_id: songId, milestone })
}
// ── PDF ──────────────────────────────────────────────────────────────────
function trackPdfOpen(trigger?: string) {
track('pdf:open', { trigger })
}
function trackPdfClose(pagesVisited: number, durationMs: number) {
track('pdf:close', { pages_visited: pagesVisited, duration_ms: durationMs })
}
// ── BookPlayer ───────────────────────────────────────────────────────────
function trackPlayerOpen(trigger?: string) {
track('player:open', { trigger })
}
function trackPlayerChapter(chapterSlug: string) {
track('player:chapter', { chapter_slug: chapterSlug })
}
function trackPlayerMode(mode: 'guided' | 'scroll') {
track('player:mode', { mode })
}
// ── Navigation & UX ──────────────────────────────────────────────────────
function trackScrollDepth(page: string, depth: 25 | 50 | 75 | 100) {
track('scroll:depth', { page, depth })
}
function trackExternalLink(url: string, label?: string) {
const route = useRoute()
track('link:external', { url, label, from_page: route.path })
}
function trackCta(label: string, target?: string) {
const route = useRoute()
track('cta:click', { label, target, from_page: route.path })
}
return {
track,
enabled,
trackAudioPlay,
trackAudioPause,
trackAudioComplete,
trackAudioProgress,
trackPdfOpen,
trackPdfClose,
trackPlayerOpen,
trackPlayerChapter,
trackPlayerMode,
trackScrollDepth,
trackExternalLink,
trackCta,
}
} }
+4
View File
@@ -27,6 +27,10 @@
</div> </div>
</template> </template>
<script setup lang="ts">
useScrollTracking()
</script>
<style scoped> <style scoped>
.app-layout { .app-layout {
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr auto;
+3 -2
View File
@@ -40,8 +40,9 @@ definePageMeta({
layout: 'default', layout: 'default',
}) })
useHead({ useSeoPage({
title: 'À propos', 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', () => 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 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) 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> </script>
<style scoped> <style scoped>
+2 -1
View File
@@ -409,8 +409,9 @@ const appConfig = useAppConfig()
const decisionUrl = (appConfig.libredecision as { url: string })?.url ?? '#' const decisionUrl = (appConfig.libredecision as { url: string })?.url ?? '#'
const sejeteral0Url = (appConfig.sejeteral0 as { url: string })?.url ?? '#' const sejeteral0Url = (appConfig.sejeteral0 as { url: string })?.url ?? '#'
useHead({ useSeoPage({
title: content.value?.meta?.title ?? 'Autonomie citoyenne', 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> </script>
+4 -2
View File
@@ -304,8 +304,10 @@
<script setup lang="ts"> <script setup lang="ts">
const { data: content } = await usePageContent('economique/commande') const { data: content } = await usePageContent('economique/commande')
useHead({ useSeoPage({
title: content.value?.meta?.title ?? 'Commander le livre', 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> </script>
+2 -1
View File
@@ -452,8 +452,9 @@ definePageMeta({
const { data: content } = await usePageContent('economique') const { data: content } = await usePageContent('economique')
useHead({ useSeoPage({
title: content.value?.meta?.title ?? 'Autonomie économique', 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) const showBookPlayer = ref(false)
+5 -2
View File
@@ -59,8 +59,11 @@ if (!chapter.value) {
throw createError({ statusCode: 404, statusMessage: 'Chapitre non trouvé' }) throw createError({ statusCode: 404, statusMessage: 'Chapitre non trouvé' })
} }
useHead({ useSeoPage({
title: chapter.value?.title, 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 // Get adjacent chapters for navigation
+5 -2
View File
@@ -424,8 +424,11 @@ definePageMeta({
const { data: content } = await usePageContent('economique/modele-eco') const { data: content } = await usePageContent('economique/modele-eco')
useHead({ useSeoPage({
title: content.value?.meta?.title ?? 'Table des matières', 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', () => 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 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) 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> </script>
<style scoped> <style scoped>
@@ -44,8 +44,9 @@
<script setup lang="ts"> <script setup lang="ts">
const { data: content } = await usePageContent('economique/productions-collectives') const { data: content } = await usePageContent('economique/productions-collectives')
useHead({ useSeoPage({
title: content.value?.meta?.title ?? 'Productions collectives', 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> </script>
+4 -2
View File
@@ -166,8 +166,10 @@ definePageMeta({
const { data: content } = await usePageContent('en-musique') const { data: content } = await usePageContent('en-musique')
const { data: homeContent } = await usePageContent('home') const { data: homeContent } = await usePageContent('home')
useHead({ useSeoPage({
title: content.value?.meta?.title ?? 'En musique', 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() const store = usePlayerStore()
+3 -2
View File
@@ -440,8 +440,9 @@ definePageMeta({
const { data: content } = await usePageContent('evenement') const { data: content } = await usePageContent('evenement')
const evtContent = computed(() => content.value as Record<string, any> | null) const evtContent = computed(() => content.value as Record<string, any> | null)
useHead({ useSeoPage({
title: evtContent.value?.meta?.title ?? 'Évènement', 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> </script>
+3 -2
View File
@@ -104,8 +104,9 @@
<script setup lang="ts"> <script setup lang="ts">
const { data: content } = await usePageContent('gratewizard') const { data: content } = await usePageContent('gratewizard')
useHead({ useSeoPage({
title: content.value?.meta?.title ?? 'grateWizard \u2014 Coefficients relatifs', 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() const { url, launch } = useGrateWizard()
+3 -2
View File
@@ -9,8 +9,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
useHead({ useSeoPage({
title: 'Accueil', 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) const showBookPlayer = ref(false)
+3 -2
View File
@@ -41,8 +41,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
useHead({ useSeoPage({
title: 'Messages', title: 'Messages — Le Librodrome',
description: 'Questions, retours et réactions des lecteurs et visiteurs du Librodrome.',
}) })
const { data: messages } = await useFetch('/api/messages') 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 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) const nextItem = computed(() => currentIdx.value < sectionItems.value.length - 1 ? sectionItems.value[currentIdx.value + 1] : null)
useHead({ useSeoPage({
title: content.value?.meta?.title ?? slug, title: content.value?.meta?.title ?? slug,
description: content.value?.description as string ?? undefined,
}) })
</script> </script>
+2 -1
View File
@@ -380,8 +380,9 @@ definePageMeta({
const { data: content } = await usePageContent('numerique') const { data: content } = await usePageContent('numerique')
useHead({ useSeoPage({
title: content.value?.meta?.title ?? 'Autonomie numérique', 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> </script>
+7 -3
View File
@@ -2,9 +2,12 @@
## Usage : docker compose -f docker-compose.yml -f docker-compose.umami.yml up -d ## Usage : docker compose -f docker-compose.yml -f docker-compose.umami.yml up -d
## ##
## Variables à définir dans .env : ## Variables à définir dans .env :
## UMAMI_DB_PASSWORD — mot de passe PostgreSQL Umami (générer avec openssl rand -hex 32) ## UMAMI_DB_PASSWORD — mot de passe PostgreSQL Umami (générer avec openssl rand -hex 32)
## UMAMI_APP_SECRET — secret applicatif (générer avec openssl rand -hex 32) ## UMAMI_APP_SECRET — secret applicatif (générer avec openssl rand -hex 32)
## UMAMI_DOMAIN — ex: stats.librodrome.org ## UMAMI_DOMAIN — ex: stats.librodrome.org
## NUXT_PUBLIC_UMAMI_URL — ex: https://stats.librodrome.org
## NUXT_PUBLIC_UMAMI_WEBSITE_ID — ID du site dans Umami (Settings → Websites)
## NUXT_UMAMI_API_KEY — clé API Umami (Settings → API Keys)
name: librodrome name: librodrome
@@ -48,5 +51,6 @@ volumes:
umami-db-data: umami-db-data:
networks: networks:
default:
traefik: traefik:
external: true external: true
+3
View File
@@ -11,6 +11,9 @@ services:
NUXT_ADMIN_PASSWORD: ${NUXT_ADMIN_PASSWORD} NUXT_ADMIN_PASSWORD: ${NUXT_ADMIN_PASSWORD}
NUXT_ADMIN_SECRET: ${NUXT_ADMIN_SECRET} NUXT_ADMIN_SECRET: ${NUXT_ADMIN_SECRET}
ADMIN_GIT_SYNC: ${ADMIN_GIT_SYNC:-false} ADMIN_GIT_SYNC: ${ADMIN_GIT_SYNC:-false}
NUXT_PUBLIC_UMAMI_URL: ${NUXT_PUBLIC_UMAMI_URL:-}
NUXT_PUBLIC_UMAMI_WEBSITE_ID: ${NUXT_PUBLIC_UMAMI_WEBSITE_ID:-}
NUXT_UMAMI_API_KEY: ${NUXT_UMAMI_API_KEY:-}
ports: ports:
- 3000 - 3000
volumes: volumes:
+40
View File
@@ -14,8 +14,48 @@ export default defineNuxtConfig({
'@unocss/nuxt', '@unocss/nuxt',
'@vueuse/nuxt', '@vueuse/nuxt',
'@nuxt/image', '@nuxt/image',
'@nuxtjs/sitemap',
], ],
site: {
url: 'https://librodrome.org',
name: 'Le Librodrome',
},
sitemap: {
strictNuxtContentPaths: false,
sources: ['/api/__sitemap__/urls'],
urls: [
{ loc: '/', changefreq: 'weekly', priority: 1.0 },
{ loc: '/economique', changefreq: 'monthly', priority: 0.9 },
{ loc: '/economique/modele-eco', changefreq: 'monthly', priority: 0.9 },
{ loc: '/economique/monnaie-libre', changefreq: 'monthly', priority: 0.8 },
{ loc: '/economique/productions-collectives', changefreq: 'monthly', priority: 0.7 },
{ loc: '/economique/commande', changefreq: 'monthly', priority: 0.8 },
{ loc: '/numerique', changefreq: 'monthly', priority: 0.8 },
{ loc: '/numerique/logiciel-libre', changefreq: 'monthly', priority: 0.8 },
{ loc: '/numerique/authentification-wot', changefreq: 'monthly', priority: 0.7 },
{ loc: '/numerique/cloud-libre', changefreq: 'monthly', priority: 0.7 },
{ loc: '/citoyenne', changefreq: 'monthly', priority: 0.8 },
{ loc: '/citoyenne/decision', changefreq: 'monthly', priority: 0.8 },
{ loc: '/citoyenne/tarifs-eau', changefreq: 'monthly', priority: 0.7 },
{ loc: '/en-musique', changefreq: 'monthly', priority: 0.8 },
{ loc: '/a-propos', changefreq: 'yearly', priority: 0.5 },
// Chapitres modele-eco
{ loc: '/economique/modele-eco/01-introduction', changefreq: 'monthly', priority: 0.7 },
{ loc: '/economique/modele-eco/02-don', changefreq: 'monthly', priority: 0.7 },
{ loc: '/economique/modele-eco/03-mesure', changefreq: 'monthly', priority: 0.7 },
{ loc: '/economique/modele-eco/04-monnaie', changefreq: 'monthly', priority: 0.7 },
{ loc: '/economique/modele-eco/05-trm', changefreq: 'monthly', priority: 0.7 },
{ loc: '/economique/modele-eco/06-produire', changefreq: 'monthly', priority: 0.7 },
{ loc: '/economique/modele-eco/07-echanger', changefreq: 'monthly', priority: 0.7 },
{ loc: '/economique/modele-eco/08-institution', changefreq: 'monthly', priority: 0.7 },
{ loc: '/economique/modele-eco/09-greffes', changefreq: 'monthly', priority: 0.7 },
{ loc: '/economique/modele-eco/10-maintenant', changefreq: 'monthly', priority: 0.7 },
{ loc: '/economique/modele-eco/11-annexes', changefreq: 'monthly', priority: 0.6 },
],
},
unocss: { unocss: {
safelist: [ safelist: [
// Axis block icons (dynamic from YAML) // Axis block icons (dynamic from YAML)
+1
View File
@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@nuxt/content": "^3.11.2", "@nuxt/content": "^3.11.2",
"@nuxt/image": "^2.0.0", "@nuxt/image": "^2.0.0",
"@nuxtjs/sitemap": "^8.0.12",
"@pinia/nuxt": "^0.11.3", "@pinia/nuxt": "^0.11.3",
"@unocss/nuxt": "^66.6.0", "@unocss/nuxt": "^66.6.0",
"@vueuse/nuxt": "^14.2.1", "@vueuse/nuxt": "^14.2.1",
+278
View File
@@ -14,6 +14,9 @@ importers:
'@nuxt/image': '@nuxt/image':
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2) version: 2.0.0(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)
'@nuxtjs/sitemap':
specifier: ^8.0.12
version: 8.0.12(@nuxt/schema@4.3.1)(magicast@0.5.2)(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))(zod@3.25.76)
'@pinia/nuxt': '@pinia/nuxt':
specifier: ^0.11.3 specifier: ^0.11.3
version: 0.11.3(magicast@0.5.2)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3))) version: 0.11.3(magicast@0.5.2)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)))
@@ -210,9 +213,15 @@ packages:
'@clack/core@1.0.1': '@clack/core@1.0.1':
resolution: {integrity: sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g==} resolution: {integrity: sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g==}
'@clack/core@1.2.0':
resolution: {integrity: sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==}
'@clack/prompts@1.0.1': '@clack/prompts@1.0.1':
resolution: {integrity: sha512-/42G73JkuYdyWZ6m8d/CJtBrGl1Hegyc7Fy78m5Ob+jF85TOUmLR5XLce/U3LxYAw0kJ8CT5aI99RIvPHcGp/Q==} resolution: {integrity: sha512-/42G73JkuYdyWZ6m8d/CJtBrGl1Hegyc7Fy78m5Ob+jF85TOUmLR5XLce/U3LxYAw0kJ8CT5aI99RIvPHcGp/Q==}
'@clack/prompts@1.2.0':
resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==}
'@cloudflare/kv-asset-handler@0.4.2': '@cloudflare/kv-asset-handler@0.4.2':
resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -733,6 +742,11 @@ packages:
peerDependencies: peerDependencies:
vite: '>=6.0' vite: '>=6.0'
'@nuxt/devtools-kit@4.0.0-alpha.3':
resolution: {integrity: sha512-ymp4jqS3hFfwRw8uDkv8cpu4kWvhQrX+S4jnA/oOc76s4AXf2HCZZJgrncKxh+txqi1NJj8nsQNBbaqRAo3g4w==}
peerDependencies:
vite: '>=6.0'
'@nuxt/devtools-wizard@3.2.1': '@nuxt/devtools-wizard@3.2.1':
resolution: {integrity: sha512-NKUg54cLQSDeBWaNwAPkVIpwXtd1CrxLr0inl9Z7OdLwsidqMrncNObO6K3HgV0PEdAcqY4IwE2hkON2dlRLYw==} resolution: {integrity: sha512-NKUg54cLQSDeBWaNwAPkVIpwXtd1CrxLr0inl9Z7OdLwsidqMrncNObO6K3HgV0PEdAcqY4IwE2hkON2dlRLYw==}
hasBin: true hasBin: true
@@ -755,6 +769,10 @@ packages:
resolution: {integrity: sha512-UjBFt72dnpc+83BV3OIbCT0YHLevJtgJCHpxMX0YRKWLDhhbcDdUse87GtsQBrjvOzK7WUNUYLDS/hQLYev5rA==} resolution: {integrity: sha512-UjBFt72dnpc+83BV3OIbCT0YHLevJtgJCHpxMX0YRKWLDhhbcDdUse87GtsQBrjvOzK7WUNUYLDS/hQLYev5rA==}
engines: {node: '>=18.12.0'} engines: {node: '>=18.12.0'}
'@nuxt/kit@4.4.2':
resolution: {integrity: sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog==}
engines: {node: '>=18.12.0'}
'@nuxt/nitro-server@4.3.1': '@nuxt/nitro-server@4.3.1':
resolution: {integrity: sha512-4aNiM69Re02gI1ywnDND0m6QdVKXhWzDdtvl/16veytdHZj3FSq57ZCwOClNJ7HQkEMqXgS+bi6S2HmJX+et+g==} resolution: {integrity: sha512-4aNiM69Re02gI1ywnDND0m6QdVKXhWzDdtvl/16veytdHZj3FSq57ZCwOClNJ7HQkEMqXgS+bi6S2HmJX+et+g==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -786,6 +804,15 @@ packages:
'@nuxtjs/mdc@0.20.1': '@nuxtjs/mdc@0.20.1':
resolution: {integrity: sha512-fGmtLDQAmmDHF5Z37Apfc3k806rb2XWDOQFhb5xlDSL7+HiiUjyFy3ctx3qWdlC08dRzfAetmsGOYbfDqYsB/w==} resolution: {integrity: sha512-fGmtLDQAmmDHF5Z37Apfc3k806rb2XWDOQFhb5xlDSL7+HiiUjyFy3ctx3qWdlC08dRzfAetmsGOYbfDqYsB/w==}
'@nuxtjs/sitemap@8.0.12':
resolution: {integrity: sha512-1lFrk7FW/+3vtWRNnAyVjhyEsQN3xwau9hQI/cTmUKyxbImY0d10ZXeicR+amCU4wSnayVeaysBjM2KREAODTA==}
engines: {node: '>=18.0.0'}
peerDependencies:
zod: '>=3'
peerDependenciesMeta:
zod:
optional: true
'@oxc-minify/binding-android-arm-eabi@0.112.0': '@oxc-minify/binding-android-arm-eabi@0.112.0':
resolution: {integrity: sha512-m7TGBR2hjsBJIN9UJ909KBoKsuogo6CuLsHKvUIBXdjI0JVHP8g4ZHeB+BJpGn5LJdeSGDfz9MWiuXrZDRzunw==} resolution: {integrity: sha512-m7TGBR2hjsBJIN9UJ909KBoKsuogo6CuLsHKvUIBXdjI0JVHP8g4ZHeB+BJpGn5LJdeSGDfz9MWiuXrZDRzunw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -1906,6 +1933,11 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
hasBin: true
agent-base@7.1.4: agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
@@ -2215,6 +2247,9 @@ packages:
cookie-es@1.2.2: cookie-es@1.2.2:
resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==}
cookie-es@1.2.3:
resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==}
cookie-es@2.0.0: cookie-es@2.0.0:
resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==}
@@ -2372,6 +2407,9 @@ packages:
defu@6.1.4: defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
denque@2.1.0: denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
@@ -2587,9 +2625,25 @@ packages:
fast-npm-meta@1.2.1: fast-npm-meta@1.2.1:
resolution: {integrity: sha512-vTHOCEbzcbQEfYL0sPzcz+HF5asxoy60tPBVaiYzsCfuyhbXZCSqXL+LgPGV22nuAYimoGMeDpywMQB4aOw8HQ==} resolution: {integrity: sha512-vTHOCEbzcbQEfYL0sPzcz+HF5asxoy60tPBVaiYzsCfuyhbXZCSqXL+LgPGV22nuAYimoGMeDpywMQB4aOw8HQ==}
fast-string-truncated-width@1.2.1:
resolution: {integrity: sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==}
fast-string-width@1.1.0:
resolution: {integrity: sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==}
fast-uri@3.1.0: fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fast-wrap-ansi@0.1.6:
resolution: {integrity: sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==}
fast-xml-builder@1.1.4:
resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==}
fast-xml-parser@5.5.11:
resolution: {integrity: sha512-QL0eb0YbSTVWF6tTf1+LEMSgtCEjBYPpnAjoLC8SscESlAjXEIRJ7cHtLG0pLeDFaZLa4VKZLArtA/60ZS7vyA==}
hasBin: true
fastq@1.20.1: fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
@@ -2733,6 +2787,9 @@ packages:
resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
h3@1.15.11:
resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==}
h3@1.15.5: h3@1.15.5:
resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==}
@@ -3338,6 +3395,9 @@ packages:
mlly@1.8.0: mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
mlly@1.8.2:
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
mocked-exports@0.1.1: mocked-exports@0.1.1:
resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==} resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==}
@@ -3444,6 +3504,12 @@ packages:
resolution: {integrity: sha512-5pVCzWXqg9HP159JDhdfQJtFvgmS/KouEVpyYLPEBXWMrQoJBwujsczmLeIKXKI2BTy4RqfXy8N1GfGTZNb57g==} resolution: {integrity: sha512-5pVCzWXqg9HP159JDhdfQJtFvgmS/KouEVpyYLPEBXWMrQoJBwujsczmLeIKXKI2BTy4RqfXy8N1GfGTZNb57g==}
hasBin: true hasBin: true
nuxt-site-config-kit@4.0.8:
resolution: {integrity: sha512-7g3giKXt0M2vssCUg8XFfR6+u4U0zywQ8p8i4msy4p+9etteFNrkrCmVHZ83xiWGFbnoTgiaymPjbaQH3KZqAg==}
nuxt-site-config@4.0.8:
resolution: {integrity: sha512-H7wHoOJ5Z6ZnTqD5vUugaKkWZbejZ9kGmzpr2dheOaC6RdT8JafCfMrmJG7W+cyJiJJ3YmzL+bzPBW2bW6MExA==}
nuxt@4.3.1: nuxt@4.3.1:
resolution: {integrity: sha512-bl+0rFcT5Ax16aiWFBFPyWcsTob19NTZaDL5P6t0MQdK63AtgS6fN6fwvwdbXtnTk6/YdCzlmuLzXhSM22h0OA==} resolution: {integrity: sha512-bl+0rFcT5Ax16aiWFBFPyWcsTob19NTZaDL5P6t0MQdK63AtgS6fN6fwvwdbXtnTk6/YdCzlmuLzXhSM22h0OA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -3457,6 +3523,20 @@ packages:
'@types/node': '@types/node':
optional: true optional: true
nuxtseo-shared@5.1.2:
resolution: {integrity: sha512-L8nYZCmdFh2w9wNf4dxQy5Vzv2JTWd661zAg3D0h9HRm3chUkMZNgWQbodE7rK6jpitydONyvi7uHXOEHbGIuA==}
peerDependencies:
'@nuxt/schema': ^3.16.0 || ^4.0.0
nuxt: ^3.16.0 || ^4.0.0
nuxt-site-config: ^3.2.0 || ^4.0.0
vue: ^3.5.0
zod: ^3.23.0 || ^4.0.0
peerDependenciesMeta:
nuxt-site-config:
optional: true
zod:
optional: true
nypm@0.6.5: nypm@0.6.5:
resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==} resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -3552,6 +3632,10 @@ packages:
path-browserify@1.0.1: path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
path-expression-matcher@1.5.0:
resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==}
engines: {node: '>=14.0.0'}
path-key@3.1.1: path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -4085,6 +4169,11 @@ packages:
sisteransi@1.0.5: sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
site-config-stack@4.0.8:
resolution: {integrity: sha512-Su+57p7CGqd3QSMmaDV+qU9EqWmgAT3SGX4Wurb5VsEBMFC3oXvai8BlrXVUnH1ay9hA1WOn0g0i6+y/RJX5Yw==}
peerDependencies:
vue: ^3.5.30
skin-tone@2.0.0: skin-tone@2.0.0:
resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -4146,6 +4235,9 @@ packages:
std-env@3.10.0: std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
std-env@4.0.0:
resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==}
streamx@2.23.0: streamx@2.23.0:
resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
@@ -4185,6 +4277,9 @@ packages:
strip-literal@3.1.0: strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
strnum@2.2.3:
resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==}
structured-clone-es@1.0.0: structured-clone-es@1.0.0:
resolution: {integrity: sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==} resolution: {integrity: sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==}
@@ -4272,6 +4367,10 @@ packages:
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
engines: {node: '>=18'} engines: {node: '>=18'}
tinyexec@1.1.1:
resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==}
engines: {node: '>=18'}
tinyglobby@0.2.15: tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -5007,12 +5106,24 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
sisteransi: 1.0.5 sisteransi: 1.0.5
'@clack/core@1.2.0':
dependencies:
fast-wrap-ansi: 0.1.6
sisteransi: 1.0.5
'@clack/prompts@1.0.1': '@clack/prompts@1.0.1':
dependencies: dependencies:
'@clack/core': 1.0.1 '@clack/core': 1.0.1
picocolors: 1.1.1 picocolors: 1.1.1
sisteransi: 1.0.5 sisteransi: 1.0.5
'@clack/prompts@1.2.0':
dependencies:
'@clack/core': 1.2.0
fast-string-width: 1.1.0
fast-wrap-ansi: 0.1.6
sisteransi: 1.0.5
'@cloudflare/kv-asset-handler@0.4.2': {} '@cloudflare/kv-asset-handler@0.4.2': {}
'@dxup/nuxt@0.3.2(magicast@0.5.2)': '@dxup/nuxt@0.3.2(magicast@0.5.2)':
@@ -5474,6 +5585,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- magicast - magicast
'@nuxt/devtools-kit@4.0.0-alpha.3(magicast@0.5.2)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))':
dependencies:
'@nuxt/kit': 4.4.2(magicast@0.5.2)
tinyexec: 1.1.1
vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)
transitivePeerDependencies:
- magicast
'@nuxt/devtools-wizard@3.2.1': '@nuxt/devtools-wizard@3.2.1':
dependencies: dependencies:
consola: 3.4.2 consola: 3.4.2
@@ -5587,6 +5706,31 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- magicast - magicast
'@nuxt/kit@4.4.2(magicast@0.5.2)':
dependencies:
c12: 3.3.3(magicast@0.5.2)
consola: 3.4.2
defu: 6.1.7
destr: 2.0.5
errx: 0.1.0
exsolve: 1.0.8
ignore: 7.0.5
jiti: 2.6.1
klona: 2.0.6
mlly: 1.8.2
ohash: 2.0.11
pathe: 2.0.3
pkg-types: 2.3.0
rc9: 3.0.0
scule: 1.3.0
semver: 7.7.4
tinyglobby: 0.2.15
ufo: 1.6.3
unctx: 2.5.0
untyped: 2.0.0
transitivePeerDependencies:
- magicast
'@nuxt/nitro-server@4.3.1(better-sqlite3@12.6.2)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)': '@nuxt/nitro-server@4.3.1(better-sqlite3@12.6.2)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)':
dependencies: dependencies:
'@nuxt/devalue': 2.0.2 '@nuxt/devalue': 2.0.2
@@ -5776,6 +5920,29 @@ snapshots:
- magicast - magicast
- supports-color - supports-color
'@nuxtjs/sitemap@8.0.12(@nuxt/schema@4.3.1)(magicast@0.5.2)(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))(zod@3.25.76)':
dependencies:
'@nuxt/kit': 4.4.2(magicast@0.5.2)
consola: 3.4.2
defu: 6.1.7
fast-xml-parser: 5.5.11
nuxt-site-config: 4.0.8(@nuxt/schema@4.3.1)(magicast@0.5.2)(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))(zod@3.25.76)
nuxtseo-shared: 5.1.2(@nuxt/schema@4.3.1)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.3.1)(magicast@0.5.2)(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))(zod@3.25.76))(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))(zod@3.25.76)
ofetch: 1.5.1
pathe: 2.0.3
pkg-types: 2.3.0
radix3: 1.1.2
ufo: 1.6.3
ultrahtml: 1.6.0
optionalDependencies:
zod: 3.25.76
transitivePeerDependencies:
- '@nuxt/schema'
- magicast
- nuxt
- vite
- vue
'@oxc-minify/binding-android-arm-eabi@0.112.0': '@oxc-minify/binding-android-arm-eabi@0.112.0':
optional: true optional: true
@@ -6814,6 +6981,8 @@ snapshots:
acorn@8.15.0: {} acorn@8.15.0: {}
acorn@8.16.0: {}
agent-base@7.1.4: {} agent-base@7.1.4: {}
ajv-formats@2.1.1(ajv@8.18.0): ajv-formats@2.1.1(ajv@8.18.0):
@@ -7111,6 +7280,8 @@ snapshots:
cookie-es@1.2.2: {} cookie-es@1.2.2: {}
cookie-es@1.2.3: {}
cookie-es@2.0.0: {} cookie-es@2.0.0: {}
copy-anything@4.0.5: copy-anything@4.0.5:
@@ -7260,6 +7431,8 @@ snapshots:
defu@6.1.4: {} defu@6.1.4: {}
defu@6.1.7: {}
denque@2.1.0: {} denque@2.1.0: {}
depd@2.0.0: {} depd@2.0.0: {}
@@ -7471,8 +7644,28 @@ snapshots:
fast-npm-meta@1.2.1: {} fast-npm-meta@1.2.1: {}
fast-string-truncated-width@1.2.1: {}
fast-string-width@1.1.0:
dependencies:
fast-string-truncated-width: 1.2.1
fast-uri@3.1.0: {} fast-uri@3.1.0: {}
fast-wrap-ansi@0.1.6:
dependencies:
fast-string-width: 1.1.0
fast-xml-builder@1.1.4:
dependencies:
path-expression-matcher: 1.5.0
fast-xml-parser@5.5.11:
dependencies:
fast-xml-builder: 1.1.4
path-expression-matcher: 1.5.0
strnum: 2.2.3
fastq@1.20.1: fastq@1.20.1:
dependencies: dependencies:
reusify: 1.1.0 reusify: 1.1.0
@@ -7611,6 +7804,18 @@ snapshots:
dependencies: dependencies:
duplexer: 0.1.2 duplexer: 0.1.2
h3@1.15.11:
dependencies:
cookie-es: 1.2.3
crossws: 0.3.5
defu: 6.1.7
destr: 2.0.5
iron-webcrypto: 1.2.1
node-mock-http: 1.0.4
radix3: 1.1.2
ufo: 1.6.3
uncrypto: 0.1.3
h3@1.15.5: h3@1.15.5:
dependencies: dependencies:
cookie-es: 1.2.2 cookie-es: 1.2.2
@@ -8500,6 +8705,13 @@ snapshots:
pkg-types: 1.3.1 pkg-types: 1.3.1
ufo: 1.6.3 ufo: 1.6.3
mlly@1.8.2:
dependencies:
acorn: 8.16.0
pathe: 2.0.3
pkg-types: 1.3.1
ufo: 1.6.3
mocked-exports@0.1.1: {} mocked-exports@0.1.1: {}
mrmime@2.0.1: {} mrmime@2.0.1: {}
@@ -8682,6 +8894,34 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- magicast - magicast
nuxt-site-config-kit@4.0.8(magicast@0.5.2)(vue@3.5.28(typescript@5.9.3)):
dependencies:
'@nuxt/kit': 4.4.2(magicast@0.5.2)
site-config-stack: 4.0.8(vue@3.5.28(typescript@5.9.3))
std-env: 4.0.0
ufo: 1.6.3
transitivePeerDependencies:
- magicast
- vue
nuxt-site-config@4.0.8(@nuxt/schema@4.3.1)(magicast@0.5.2)(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))(zod@3.25.76):
dependencies:
'@nuxt/kit': 4.4.2(magicast@0.5.2)
h3: 1.15.11
nuxt-site-config-kit: 4.0.8(magicast@0.5.2)(vue@3.5.28(typescript@5.9.3))
nuxtseo-shared: 5.1.2(@nuxt/schema@4.3.1)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.3.1)(magicast@0.5.2)(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))(zod@3.25.76))(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))(zod@3.25.76)
pathe: 2.0.3
pkg-types: 2.3.0
site-config-stack: 4.0.8(vue@3.5.28(typescript@5.9.3))
ufo: 1.6.3
transitivePeerDependencies:
- '@nuxt/schema'
- magicast
- nuxt
- vite
- vue
- zod
nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2): nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2):
dependencies: dependencies:
'@dxup/nuxt': 0.3.2(magicast@0.5.2) '@dxup/nuxt': 0.3.2(magicast@0.5.2)
@@ -8805,6 +9045,31 @@ snapshots:
- xml2js - xml2js
- yaml - yaml
nuxtseo-shared@5.1.2(@nuxt/schema@4.3.1)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.3.1)(magicast@0.5.2)(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))(zod@3.25.76))(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))(zod@3.25.76):
dependencies:
'@clack/prompts': 1.2.0
'@nuxt/devtools-kit': 4.0.0-alpha.3(magicast@0.5.2)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))
'@nuxt/kit': 4.4.2(magicast@0.5.2)
'@nuxt/schema': 4.3.1
birpc: 4.0.0
consola: 3.4.2
defu: 6.1.7
nuxt: 4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2)
ofetch: 1.5.1
pathe: 2.0.3
pkg-types: 2.3.0
radix3: 1.1.2
sirv: 3.0.2
std-env: 4.0.0
ufo: 1.6.3
vue: 3.5.28(typescript@5.9.3)
optionalDependencies:
nuxt-site-config: 4.0.8(@nuxt/schema@4.3.1)(magicast@0.5.2)(nuxt@4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))(zod@3.25.76)
zod: 3.25.76
transitivePeerDependencies:
- magicast
- vite
nypm@0.6.5: nypm@0.6.5:
dependencies: dependencies:
citty: 0.2.1 citty: 0.2.1
@@ -8971,6 +9236,8 @@ snapshots:
path-browserify@1.0.1: {} path-browserify@1.0.1: {}
path-expression-matcher@1.5.0: {}
path-key@3.1.1: {} path-key@3.1.1: {}
path-key@4.0.0: {} path-key@4.0.0: {}
@@ -9633,6 +9900,11 @@ snapshots:
sisteransi@1.0.5: {} sisteransi@1.0.5: {}
site-config-stack@4.0.8(vue@3.5.28(typescript@5.9.3)):
dependencies:
ufo: 1.6.3
vue: 3.5.28(typescript@5.9.3)
skin-tone@2.0.0: skin-tone@2.0.0:
dependencies: dependencies:
unicode-emoji-modifier-base: 1.0.0 unicode-emoji-modifier-base: 1.0.0
@@ -9684,6 +9956,8 @@ snapshots:
std-env@3.10.0: {} std-env@3.10.0: {}
std-env@4.0.0: {}
streamx@2.23.0: streamx@2.23.0:
dependencies: dependencies:
events-universal: 1.0.1 events-universal: 1.0.1
@@ -9734,6 +10008,8 @@ snapshots:
dependencies: dependencies:
js-tokens: 9.0.1 js-tokens: 9.0.1
strnum@2.2.3: {}
structured-clone-es@1.0.0: {} structured-clone-es@1.0.0: {}
stylehacks@7.0.7(postcss@8.5.6): stylehacks@7.0.7(postcss@8.5.6):
@@ -9828,6 +10104,8 @@ snapshots:
tinyexec@1.0.2: {} tinyexec@1.0.2: {}
tinyexec@1.1.1: {}
tinyglobby@0.2.15: tinyglobby@0.2.15:
dependencies: dependencies:
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+1 -1
View File
@@ -3,7 +3,7 @@ book:
author: Yvv author: Yvv
pdfFile: /pdf/une-economie-du-don.pdf pdfFile: /pdf/une-economie-du-don.pdf
description: Un livre et 9 chansons pour explorer ensemble les fondements d'une économie fondée sur le don. description: Un livre et 9 chansons pour explorer ensemble les fondements d'une économie fondée sur le don.
coverImage: /images/book-cover.jpg coverImage: /images/Couv-Economie-du-don.jpg
license: CC-BY-NC license: CC-BY-NC
isbn: 979-1-042-45206-3 isbn: 979-1-042-45206-3
songs: songs:
+1 -1
View File
@@ -31,7 +31,7 @@ book:
title: Une économie du don — enfin concevable title: Une économie du don — enfin concevable
description: Un livre et quelques chansons pour une proposition de modèle économique fondé sur le don. Le livre est description: Un livre et quelques chansons pour une proposition de modèle économique fondé sur le don. Le livre est
accompagné de chansons qui le racontent, un peu autrement. accompagné de chansons qui le racontent, un peu autrement.
coverImage: /images/book-cover-spread.jpg coverImage: /images/Couv-Economie-du-don.jpg
coverAlt: Couverture — Une économie du don, enfin concevable coverAlt: Couverture — Une économie du don, enfin concevable
cta: cta:
player: Présentation musicale player: Présentation musicale