Compare commits
10 Commits
efed0b9033
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 98fab43fe5 | |||
| a334c97434 | |||
| 84e843479d | |||
| 2f444ea7c7 | |||
| 8408fd6466 | |||
| dcf64cc924 | |||
| f6339400fa | |||
| 8fd000a153 | |||
| 95f82e4fee | |||
| a038851895 |
@@ -1,3 +1,9 @@
|
||||
# Admin authentication
|
||||
NUXT_ADMIN_PASSWORD=changeme
|
||||
NUXT_ADMIN_SECRET=change-this-to-a-random-secret-at-least-32-chars
|
||||
|
||||
# Umami analytics — hébergé sur stats.open.us.org
|
||||
# NUXT_UMAMI_API_KEY : Settings → API Keys dans l'interface Umami (pour /api/stats)
|
||||
NUXT_PUBLIC_UMAMI_URL=https://stats.open.us.org
|
||||
NUXT_PUBLIC_UMAMI_WEBSITE_ID=95ff616d-9ce1-47d9-bca2-f6ddc344a99a
|
||||
NUXT_UMAMI_API_KEY=
|
||||
|
||||
@@ -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 `<a target="_blank">` : capturé automatiquement par `useScrollTracking()` dans `default.vue`
|
||||
- Tout nouveau lecteur média : appeler `trackAudioPlay` / `trackPdfOpen` depuis `useTracking()`
|
||||
|
||||
+2
-3
@@ -81,9 +81,8 @@ Parent avec `overflow: clip` (pas `overflow: hidden` qui casserait `position: st
|
||||
|
||||
## Analytics
|
||||
|
||||
`app/composables/useTracking.ts` — wrapper Umami. Activé si `NUXT_PUBLIC_UMAMI_WEBSITE_ID` est défini.
|
||||
`server/api/stats/index.get.ts` — endpoint public pour la fédération inter-instances (observatoires territoire).
|
||||
`docker/docker-compose.umami.yml` — stack Umami + PostgreSQL avec labels Traefik.
|
||||
`app/composables/useTracking.ts` — wrapper Umami. Module `nuxt-umami` configuré dans `nuxt.config.ts` (host : `stats.open.us.org`).
|
||||
`server/api/stats/index.get.ts` — endpoint public stats (nécessite `NUXT_UMAMI_API_KEY`).
|
||||
|
||||
## Redirections
|
||||
|
||||
|
||||
@@ -64,9 +64,8 @@ Port réservé : **3000** (ne pas changer).
|
||||
|
||||
## Analytics
|
||||
|
||||
Umami self-hosted (optionnel). Configurer `NUXT_PUBLIC_UMAMI_WEBSITE_ID` et `NUXT_PUBLIC_UMAMI_URL` dans l'environnement.
|
||||
Déploiement séparé : `docker/docker-compose.umami.yml` → `stats.librodrome.org`.
|
||||
Stats publiques exposées via `/api/stats` pour la fédération inter-instances.
|
||||
Umami hébergé sur `stats.open.us.org`. Module `nuxt-umami` configuré dans `nuxt.config.ts`.
|
||||
Stats publiques exposées via `/api/stats` (nécessite `NUXT_UMAMI_API_KEY`).
|
||||
|
||||
## Déploiement
|
||||
|
||||
|
||||
+52
-16
@@ -17,24 +17,60 @@
|
||||
const paletteStore = usePaletteStore()
|
||||
onMounted(() => paletteStore.applyToDOM())
|
||||
|
||||
// Umami analytics — inject script only when configured
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
if (runtimeConfig.public.umamiWebsiteId && runtimeConfig.public.umamiUrl) {
|
||||
useHead({
|
||||
script: [{
|
||||
src: `${runtimeConfig.public.umamiUrl}/script.js`,
|
||||
defer: true,
|
||||
'data-website-id': runtimeConfig.public.umamiWebsiteId,
|
||||
}],
|
||||
})
|
||||
}
|
||||
const config = useRuntimeConfig()
|
||||
const siteUrl = (config.public.siteUrl as string) || 'https://librodrome.org'
|
||||
|
||||
// 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/',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="pdf-overlay">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="pdf-reader"
|
||||
@keydown.escape="close"
|
||||
tabindex="0"
|
||||
ref="overlayRef"
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="pdf-reader"
|
||||
@keydown.escape="close"
|
||||
tabindex="0"
|
||||
ref="overlayRef"
|
||||
>
|
||||
<!-- Top bar -->
|
||||
<div class="pdf-bar">
|
||||
<div class="pdf-bar-title">
|
||||
@@ -26,10 +25,11 @@
|
||||
:src="pdfUrl"
|
||||
class="pdf-frame"
|
||||
:title="bpContent?.pdf.iframeTitle"
|
||||
ref="iframeRef"
|
||||
@load="iframeRef?.focus()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
@@ -40,8 +40,10 @@ 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<HTMLElement>()
|
||||
const iframeRef = ref<HTMLIFrameElement>()
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
@@ -59,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' : ''
|
||||
@@ -138,15 +149,4 @@ onUnmounted(() => {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Overlay transitions */
|
||||
.pdf-overlay-enter-active {
|
||||
animation: pdf-enter 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
.pdf-overlay-leave-active {
|
||||
animation: pdf-enter 0.3s cubic-bezier(0.7, 0, 0.84, 0) reverse both;
|
||||
}
|
||||
@keyframes pdf-enter {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
<!-- Song disc -->
|
||||
<div v-if="currentSong" class="reader-song">
|
||||
<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>
|
||||
<span class="reader-song-name">{{ currentSong.title }}</span>
|
||||
@@ -138,6 +138,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
const { data: bpContent } = await usePageContent('book-player')
|
||||
const { trackPlayerOpen, trackPlayerChapter, trackPlayerMode } = useTracking()
|
||||
|
||||
const COL_GAP = 80
|
||||
|
||||
@@ -167,6 +168,7 @@ const scrollPercent = ref(0)
|
||||
|
||||
// When switching back to paginated, recalc pages
|
||||
watch(readingMode, async (mode) => {
|
||||
trackPlayerMode(mode === 'scroll' ? 'scroll' : 'guided')
|
||||
if (mode === 'paginated') {
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
@@ -288,6 +290,7 @@ function goToTrack(idx: number) {
|
||||
// Play the song
|
||||
const song = tracks.value[idx]?.song
|
||||
if (song) {
|
||||
trackPlayerChapter(song.id)
|
||||
_skipSongWatch = true
|
||||
audioPlayer.loadAndPlay(song)
|
||||
}
|
||||
@@ -378,6 +381,7 @@ function onTouchEnd(e: TouchEvent) {
|
||||
watch(isOpen, async (open) => {
|
||||
if (open) {
|
||||
showSommaire.value = false
|
||||
trackPlayerOpen()
|
||||
await initBookData()
|
||||
await nextTick()
|
||||
overlayRef.value?.focus()
|
||||
|
||||
@@ -16,12 +16,17 @@
|
||||
class="axis-item card-surface"
|
||||
:class="{ 'axis-item--gestation': item.gestation }"
|
||||
>
|
||||
<!-- Clickable card body -->
|
||||
<!-- Overlay link — full card clickable (z-index 0) -->
|
||||
<component
|
||||
:is="itemTag(item)"
|
||||
v-bind="itemAttrs(item)"
|
||||
class="axis-item-body"
|
||||
>
|
||||
class="axis-item-overlay"
|
||||
:aria-label="item.label"
|
||||
tabindex="-1"
|
||||
/>
|
||||
|
||||
<!-- Card body (position relative, z-index 1 — above overlay) -->
|
||||
<div class="axis-item-body">
|
||||
<!-- Item icon -->
|
||||
<div v-if="item.icon" class="axis-item-icon" :class="`axis-item-icon--${color}`">
|
||||
<span v-if="item.icon === 'g1'" class="axis-item-icon-g1">Ğ1</span>
|
||||
@@ -46,9 +51,9 @@
|
||||
</div>
|
||||
<p class="axis-pi-text">{{ item.presentation.text }}</p>
|
||||
</div>
|
||||
</component>
|
||||
</div>
|
||||
|
||||
<!-- Actions zone (separate from card link) -->
|
||||
<!-- Actions zone — z-index 1, above overlay, boutons ouvrent liens externes -->
|
||||
<div v-if="item.actions?.length" class="axis-actions">
|
||||
<!-- Primary row -->
|
||||
<div class="axis-actions-row">
|
||||
@@ -67,17 +72,25 @@
|
||||
</div>
|
||||
<!-- Secondary row -->
|
||||
<div v-if="secondaryActions(item.actions).length" class="axis-actions-secondary">
|
||||
<component
|
||||
:is="action.to ? resolveComponent('NuxtLink') : 'button'"
|
||||
v-for="action in secondaryActions(item.actions)"
|
||||
:key="action.label"
|
||||
:to="action.to"
|
||||
class="axis-action-btn axis-action-btn--secondary"
|
||||
@click.stop="!action.to && handleAction(action.id)"
|
||||
>
|
||||
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
|
||||
{{ action.label }}
|
||||
</component>
|
||||
<template v-for="action in secondaryActions(item.actions)" :key="action.label">
|
||||
<NuxtLink
|
||||
v-if="action.to"
|
||||
:to="action.to"
|
||||
class="axis-action-btn axis-action-btn--secondary"
|
||||
@click.stop
|
||||
>
|
||||
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
|
||||
{{ action.label }}
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-else
|
||||
class="axis-action-btn axis-action-btn--secondary"
|
||||
@click.stop="handleAction(action.id)"
|
||||
>
|
||||
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,9 +198,18 @@ function itemAttrs(item: AxisItem) {
|
||||
border: 1px solid hsl(var(--color-text) / 0.08);
|
||||
background: hsl(var(--color-surface));
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
/* overflow: visible pour laisser le tooltip sortir du cadre */
|
||||
}
|
||||
|
||||
/* Lien overlay — couvre toute la carte, AU-DESSUS du contenu */
|
||||
.axis-item-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.axis-item:hover {
|
||||
border-color: hsl(var(--color-primary) / 0.25);
|
||||
box-shadow: 0 4px 24px hsl(var(--color-primary) / 0.06);
|
||||
@@ -206,7 +228,6 @@ function itemAttrs(item: AxisItem) {
|
||||
flex-direction: column;
|
||||
padding: 1.25rem;
|
||||
flex: 1;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@@ -262,6 +283,8 @@ function itemAttrs(item: AxisItem) {
|
||||
background: hsl(var(--color-bg) / 0.4);
|
||||
border-bottom-left-radius: 0.75rem;
|
||||
border-bottom-right-radius: 0.75rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.axis-actions-row {
|
||||
|
||||
@@ -131,10 +131,9 @@ const { data: content } = await usePageContent('home')
|
||||
}
|
||||
|
||||
.book-cover-img {
|
||||
width: 200%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: translateX(-50%);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.heading-section {
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
<!-- Book cover -->
|
||||
<UiScrollReveal>
|
||||
<div class="book-cover-wrapper relative">
|
||||
<div :class="['book-cover-3d', compact && 'book-cover-3d--compact']">
|
||||
<img
|
||||
:src="content?.book.coverImage"
|
||||
:alt="content?.book.coverAlt"
|
||||
class="book-cover-img"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="['book-cover-3d', compact && 'book-cover-3d--compact']"
|
||||
:style="{ backgroundImage: `url(${content?.book.coverImage})` }"
|
||||
role="img"
|
||||
:aria-label="content?.book.coverAlt"
|
||||
/>
|
||||
</div>
|
||||
</UiScrollReveal>
|
||||
|
||||
@@ -88,7 +87,12 @@ const titleLine2 = computed(() => titleParts.value[1])
|
||||
}
|
||||
|
||||
.book-cover-3d {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
aspect-ratio: 3 / 4;
|
||||
background-size: 200% auto;
|
||||
background-position: right center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid hsl(var(--color-text) / 0.1);
|
||||
@@ -97,7 +101,6 @@ const titleLine2 = computed(() => titleParts.value[1])
|
||||
0 0 0 1px hsl(var(--color-text) / 0.08);
|
||||
transition: transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
|
||||
box-shadow 0.5s ease;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.book-cover-3d--compact {
|
||||
@@ -111,12 +114,6 @@ const titleLine2 = computed(() => titleParts.value[1])
|
||||
0 0 0 1px hsl(var(--color-primary) / 0.2);
|
||||
}
|
||||
|
||||
.book-cover-img {
|
||||
width: 200%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.book-heading {
|
||||
font-size: clamp(1.625rem, 4vw, 2.125rem);
|
||||
|
||||
@@ -3,13 +3,21 @@
|
||||
<div class="container-content flex h-[var(--header-height)] items-center justify-between px-4">
|
||||
<!-- Logo -->
|
||||
<NuxtLink to="/" class="logo-link flex items-center gap-2.5">
|
||||
<svg class="logo-icon" viewBox="0 0 64 80" fill="none" aria-hidden="true">
|
||||
<path d="M38 8 C28 6 18 10 18 20 C18 28 26 32 34 34 C42 36 48 40 48 48 C48 52 46 55 42 57 L44 40 C44 36 40 32 34 30 C28 28 22 24 22 18 C22 14 24 11 28 10Z" fill="currentColor" opacity="0.9"/>
|
||||
<path d="M26 72 C36 74 46 70 46 60 C46 52 38 48 30 46 C22 44 16 40 16 32 C16 28 18 25 22 23 L20 40 C20 44 24 48 30 50 C36 52 42 56 42 62 C42 66 40 69 36 70Z" fill="currentColor" opacity="0.9"/>
|
||||
<path d="M20 16 C20 8 28 4 36 6 C42 8 46 14 44 20" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" fill="none" opacity="0.7"/>
|
||||
<path d="M44 64 C44 72 36 76 28 74 C22 72 18 66 20 60" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" fill="none" opacity="0.7"/>
|
||||
<path d="M36 4 Q42 2 46 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.5"/>
|
||||
<path d="M28 76 Q22 78 18 74" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.5"/>
|
||||
<svg class="logo-icon" viewBox="0 0 46 78" fill="none" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="sect-grad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="hsl(var(--color-primary))"/>
|
||||
<stop offset="100%" stop-color="hsl(var(--color-accent))"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M 33 10 C 26 7 18 7 14 11 C 8 15 7 24 10 31 C 13 38 21 40 27 44 C 33 48 38 55 35 62 C 32 69 24 72 17 70 C 10 68 8 72 10 75 C 12 78 20 79 28 76"
|
||||
stroke="url(#sect-grad)"
|
||||
stroke-width="5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
<span class="logo-text">{{ site?.identity.name }}</span>
|
||||
</NuxtLink>
|
||||
@@ -78,7 +86,6 @@ const allNav = computed(() => [...axes.value, ...extra.value])
|
||||
.logo-icon {
|
||||
width: 1.6rem;
|
||||
height: 2rem;
|
||||
color: hsl(var(--color-primary));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@ import type { Song } from '~/types/song'
|
||||
|
||||
let audio: HTMLAudioElement | 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() {
|
||||
const store = usePlayerStore()
|
||||
const { trackAudioPlay, trackAudioComplete, trackAudioProgress } = useTracking()
|
||||
|
||||
function getAudio(): HTMLAudioElement {
|
||||
if (!audio) {
|
||||
@@ -17,6 +20,10 @@ export function useAudioPlayer() {
|
||||
})
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
if (store.currentSong) {
|
||||
trackAudioComplete(store.currentSong.id, store.currentSong.title)
|
||||
}
|
||||
firedMilestones.clear()
|
||||
const next = store.nextSong()
|
||||
if (next) {
|
||||
loadAndPlay(next)
|
||||
@@ -36,6 +43,16 @@ export function useAudioPlayer() {
|
||||
const update = () => {
|
||||
if (audio && !audio.paused) {
|
||||
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)
|
||||
}
|
||||
@@ -52,6 +69,7 @@ export function useAudioPlayer() {
|
||||
async function loadAndPlay(song: Song) {
|
||||
const el = getAudio()
|
||||
store.setSong(song)
|
||||
firedMilestones.clear()
|
||||
|
||||
// Try OGG first, fall back to MP3
|
||||
const oggPath = song.file.replace(/\.mp3$/, '.ogg')
|
||||
@@ -64,6 +82,7 @@ export function useAudioPlayer() {
|
||||
await el.play()
|
||||
store.play()
|
||||
startTimeUpdate()
|
||||
trackAudioPlay(song.id, song.title)
|
||||
}
|
||||
catch {
|
||||
// If OGG failed, try MP3
|
||||
@@ -73,6 +92,7 @@ export function useAudioPlayer() {
|
||||
await el.play()
|
||||
store.play()
|
||||
startTimeUpdate()
|
||||
trackAudioPlay(song.id, song.title)
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Playback failed:', err)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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 }],
|
||||
})
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
/**
|
||||
* Umami analytics wrapper — safe server-side, no-op when not configured.
|
||||
* Usage: const { track } = useTracking()
|
||||
* track('player:open')
|
||||
* track('axis:navigate', { axis: 'numerique' })
|
||||
*
|
||||
* Events convention:
|
||||
* 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() {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const enabled = !!runtimeConfig.public.umamiWebsiteId
|
||||
|
||||
// ── Core ────────────────────────────────────────────────────────────────
|
||||
function track(event: string, data?: Record<string, unknown>) {
|
||||
if (!import.meta.client || !enabled) return
|
||||
const umami = (window as Record<string, unknown>).umami as
|
||||
@@ -16,5 +22,75 @@ export function useTracking() {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useScrollTracking()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-layout {
|
||||
grid-template-rows: auto 1fr auto;
|
||||
|
||||
@@ -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', () =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -343,7 +343,8 @@
|
||||
<div class="mx-auto max-w-3xl flex flex-col gap-6">
|
||||
<!-- Decision collective -->
|
||||
<div class="item-card">
|
||||
<NuxtLink to="/citoyenne/decision" class="item-body group">
|
||||
<NuxtLink to="/citoyenne/decision" class="item-overlay" aria-label="Décision collective" tabindex="-1" />
|
||||
<div class="item-body group">
|
||||
<div class="item-header">
|
||||
<div class="item-icon">
|
||||
<div class="i-lucide-gavel h-5 w-5" />
|
||||
@@ -355,11 +356,7 @@
|
||||
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
|
||||
Se donner les moyens de la décision collective.
|
||||
</p>
|
||||
<div class="mt-3 inline-flex items-center gap-1 text-sm text-primary group-hover:text-primary/80 transition-colors">
|
||||
En savoir plus
|
||||
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<a :href="decisionUrl" target="_blank" rel="noopener" class="action-btn action-btn--primary">
|
||||
<div class="i-lucide-external-link h-3.5 w-3.5" />
|
||||
@@ -370,7 +367,8 @@
|
||||
|
||||
<!-- Tarifs de l'eau -->
|
||||
<div class="item-card">
|
||||
<NuxtLink to="/citoyenne/tarifs-eau" class="item-body group">
|
||||
<NuxtLink to="/citoyenne/tarifs-eau" class="item-overlay" aria-label="Tarifs de l'eau" tabindex="-1" />
|
||||
<div class="item-body group">
|
||||
<div class="item-header">
|
||||
<div class="item-icon">
|
||||
<div class="i-lucide-droplets h-5 w-5" />
|
||||
@@ -387,11 +385,7 @@
|
||||
Application pour obtenir justice sociale et incitation dynamique à la réduction.
|
||||
Permet de confier la décision à la population des communes.
|
||||
</p>
|
||||
<div class="mt-3 inline-flex items-center gap-1 text-sm text-primary group-hover:text-primary/80 transition-colors">
|
||||
En savoir plus
|
||||
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<a :href="sejeteral0Url" target="_blank" rel="noopener" class="action-btn action-btn--primary">
|
||||
<div class="i-lucide-external-link h-3.5 w-3.5" />
|
||||
@@ -415,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>
|
||||
|
||||
@@ -431,16 +426,24 @@ useHead({
|
||||
background: hsl(var(--color-surface));
|
||||
transition: border-color 0.2s;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
border-color: hsl(var(--color-primary) / 0.2);
|
||||
}
|
||||
|
||||
/* Lien overlay — couvre toute la carte, AU-DESSUS du contenu */
|
||||
.item-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.item-body {
|
||||
display: block;
|
||||
padding: 1.5rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@@ -484,6 +487,8 @@ useHead({
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-top: 1px solid hsl(var(--color-text) / 0.06);
|
||||
background: hsl(var(--color-bg) / 0.4);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -43,6 +43,11 @@ definePageMeta({
|
||||
const route = useRoute()
|
||||
const slug = route.params.slug as string
|
||||
|
||||
// Scroll to top when navigating between chapters (component reused by Vue Router)
|
||||
watch(() => slug, () => {
|
||||
if (import.meta.client) window.scrollTo({ top: 0, behavior: 'instant' })
|
||||
})
|
||||
|
||||
// Initialize guided mode
|
||||
useGuidedMode()
|
||||
|
||||
@@ -54,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
|
||||
|
||||
@@ -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', () =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -599,6 +599,13 @@
|
||||
|
||||
</div><!-- /sections-area -->
|
||||
|
||||
<div v-if="slug === 'logiciel-libre'" class="text-center mt-6">
|
||||
<UiBaseButton href="https://axiom-team.fr/dons" target="_blank">
|
||||
<div class="i-lucide-external-link mr-2 h-4 w-4" />
|
||||
Voir la v1 bridée (premier flux)
|
||||
</UiBaseButton>
|
||||
</div>
|
||||
|
||||
<div v-if="slug === 'tarifs-eau'" class="text-center mt-6">
|
||||
<UiBaseButton :href="sejeteral0Url" target="_blank">
|
||||
<div class="i-lucide-external-link mr-2 h-4 w-4" />
|
||||
@@ -640,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>
|
||||
|
||||
|
||||
+100
-30
@@ -320,40 +320,52 @@
|
||||
:key="pillar.id"
|
||||
class="pillar-card"
|
||||
>
|
||||
<div class="pillar-header">
|
||||
<div class="pillar-icon">
|
||||
<div :class="`i-lucide-${pillar.icon}`" class="h-5 w-5" />
|
||||
</div>
|
||||
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
|
||||
{{ pillar.label }}
|
||||
</h2>
|
||||
<span v-if="pillar.gestation" class="gestation-badge">
|
||||
<div class="i-lucide-flask-conical h-3 w-3" />
|
||||
En gestation
|
||||
</span>
|
||||
</div>
|
||||
<!-- Overlay lien — toute la carte cliquable -->
|
||||
<NuxtLink v-if="pillar.to" :to="pillar.to" class="pillar-overlay" :aria-label="pillar.label" tabindex="-1" />
|
||||
|
||||
<p class="leading-relaxed whitespace-pre-line mt-3" style="color: hsl(var(--color-text-muted))">{{ pillar.text }}</p>
|
||||
|
||||
<!-- Project card -->
|
||||
<div v-if="pillar.project" class="project-card mt-4">
|
||||
<div class="project-icon">
|
||||
<div class="i-lucide-rocket h-4 w-4" />
|
||||
<!-- Corps de la carte -->
|
||||
<div class="pillar-body">
|
||||
<div class="pillar-header">
|
||||
<div class="pillar-icon">
|
||||
<div :class="`i-lucide-${pillar.icon}`" class="h-5 w-5" />
|
||||
</div>
|
||||
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
|
||||
{{ pillar.label }}
|
||||
</h2>
|
||||
<span v-if="pillar.gestation" class="gestation-badge">
|
||||
<div class="i-lucide-flask-conical h-3 w-3" />
|
||||
En gestation
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-display font-semibold text-sm" style="color: hsl(var(--color-text))">{{ pillar.project.name }}</span>
|
||||
<span class="text-sm ml-2" style="color: hsl(var(--color-text-muted))">{{ pillar.project.text }}</span>
|
||||
|
||||
<p class="leading-relaxed whitespace-pre-line mt-3" style="color: hsl(var(--color-text-muted))">{{ pillar.text }}</p>
|
||||
|
||||
<!-- Project card -->
|
||||
<div v-if="pillar.project" class="project-card mt-4">
|
||||
<div class="project-icon">
|
||||
<div class="i-lucide-rocket h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-display font-semibold text-sm" style="color: hsl(var(--color-text))">{{ pillar.project.name }}</span>
|
||||
<span class="text-sm ml-2" style="color: hsl(var(--color-text-muted))">{{ pillar.project.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="pillar.to" class="mt-4">
|
||||
<NuxtLink
|
||||
:to="pillar.to"
|
||||
class="inline-flex items-center gap-1 text-sm text-primary hover:text-primary/80 transition-colors"
|
||||
<!-- Zone actions — z-index au-dessus de l'overlay -->
|
||||
<div v-if="(pillar as any).actions?.length" class="pillar-actions">
|
||||
<a
|
||||
v-for="action in (pillar as any).actions"
|
||||
:key="action.label"
|
||||
:href="action.href"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="pillar-action-btn"
|
||||
:class="{ 'pillar-action-btn--highlight': action.highlight }"
|
||||
>
|
||||
En savoir plus
|
||||
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
|
||||
</NuxtLink>
|
||||
<div :class="`i-lucide-${action.icon}`" class="h-3.5 w-3.5" />
|
||||
{{ action.label }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -368,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>
|
||||
|
||||
@@ -379,17 +392,74 @@ useHead({
|
||||
}
|
||||
|
||||
.pillar-card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid hsl(var(--color-text) / 0.08);
|
||||
background: hsl(var(--color-surface));
|
||||
transition: border-color 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pillar-card:hover {
|
||||
border-color: hsl(var(--color-primary) / 0.2);
|
||||
}
|
||||
|
||||
.pillar-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.pillar-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.pillar-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-top: 1px solid hsl(var(--color-text) / 0.06);
|
||||
background: hsl(var(--color-bg) / 0.4);
|
||||
border-bottom-left-radius: 0.75rem;
|
||||
border-bottom-right-radius: 0.75rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.pillar-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-text) / 0.7);
|
||||
background: hsl(var(--color-text) / 0.05);
|
||||
border: 1px solid hsl(var(--color-text) / 0.1);
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pillar-action-btn:hover {
|
||||
color: hsl(var(--color-text));
|
||||
background: hsl(var(--color-primary) / 0.12);
|
||||
border-color: hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.pillar-action-btn--highlight {
|
||||
color: hsl(var(--color-primary));
|
||||
background: hsl(var(--color-primary) / 0.12);
|
||||
border-color: hsl(var(--color-primary) / 0.25);
|
||||
}
|
||||
|
||||
.pillar-action-btn--highlight:hover {
|
||||
background: hsl(var(--color-primary) / 0.2);
|
||||
border-color: hsl(var(--color-primary) / 0.4);
|
||||
}
|
||||
|
||||
.pillar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { RouterConfig } from '@nuxt/schema'
|
||||
|
||||
export default <RouterConfig>{
|
||||
scrollBehavior(to, _from, savedPosition) {
|
||||
if (savedPosition) return savedPosition
|
||||
if (to.hash) return { el: to.hash, behavior: 'smooth' }
|
||||
return { top: 0, behavior: 'instant' }
|
||||
},
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
## Umami analytics — déployer avec le compose principal
|
||||
## Usage : docker compose -f docker-compose.yml -f docker-compose.umami.yml up -d
|
||||
##
|
||||
## Variables à définir dans .env :
|
||||
## 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_DOMAIN — ex: stats.librodrome.org
|
||||
|
||||
name: librodrome
|
||||
|
||||
services:
|
||||
umami:
|
||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||
environment:
|
||||
DATABASE_URL: postgresql://umami:${UMAMI_DB_PASSWORD}@umami-db:5432/umami
|
||||
DATABASE_TYPE: postgresql
|
||||
APP_SECRET: ${UMAMI_APP_SECRET}
|
||||
depends_on:
|
||||
umami-db:
|
||||
condition: service_healthy
|
||||
restart: always
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.umami.rule=Host(`${UMAMI_DOMAIN:-stats.librodrome.org}`)"
|
||||
- "traefik.http.routers.umami.entrypoints=websecure"
|
||||
- "traefik.http.routers.umami.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.umami.loadbalancer.server.port=3000"
|
||||
networks:
|
||||
- default
|
||||
- traefik
|
||||
|
||||
umami-db:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_DB: umami
|
||||
POSTGRES_USER: umami
|
||||
POSTGRES_PASSWORD: ${UMAMI_DB_PASSWORD}
|
||||
volumes:
|
||||
- umami-db-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U umami"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
umami-db-data:
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
@@ -11,6 +11,9 @@ services:
|
||||
NUXT_ADMIN_PASSWORD: ${NUXT_ADMIN_PASSWORD}
|
||||
NUXT_ADMIN_SECRET: ${NUXT_ADMIN_SECRET}
|
||||
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:
|
||||
- 3000
|
||||
volumes:
|
||||
|
||||
@@ -14,8 +14,56 @@ export default defineNuxtConfig({
|
||||
'@unocss/nuxt',
|
||||
'@vueuse/nuxt',
|
||||
'@nuxt/image',
|
||||
'@nuxtjs/sitemap',
|
||||
'nuxt-umami',
|
||||
],
|
||||
|
||||
umami: {
|
||||
host: 'https://stats.open.us.org',
|
||||
id: '95ff616d-9ce1-47d9-bca2-f6ddc344a99a',
|
||||
autoTrack: true,
|
||||
ignoreLocalhost: true,
|
||||
},
|
||||
|
||||
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: {
|
||||
safelist: [
|
||||
// Axis block icons (dynamic from YAML)
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
"dependencies": {
|
||||
"@nuxt/content": "^3.11.2",
|
||||
"@nuxt/image": "^2.0.0",
|
||||
"@nuxtjs/sitemap": "^8.0.12",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"@unocss/nuxt": "^66.6.0",
|
||||
"@vueuse/nuxt": "^14.2.1",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"nuxt": "^4.3.1",
|
||||
"nuxt-umami": "^3.2.1",
|
||||
"pdfjs-dist": "^5.4.624",
|
||||
"vue": "^3.5.28",
|
||||
"vue-router": "^4.6.4",
|
||||
|
||||
Generated
+330
-4
@@ -14,6 +14,9 @@ importers:
|
||||
'@nuxt/image':
|
||||
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)
|
||||
'@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':
|
||||
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)))
|
||||
@@ -29,6 +32,9 @@ importers:
|
||||
nuxt:
|
||||
specifier: ^4.3.1
|
||||
version: 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-umami:
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.1(magicast@0.5.2)
|
||||
pdfjs-dist:
|
||||
specifier: ^5.4.624
|
||||
version: 5.4.624
|
||||
@@ -210,9 +216,15 @@ packages:
|
||||
'@clack/core@1.0.1':
|
||||
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':
|
||||
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':
|
||||
resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -733,6 +745,11 @@ packages:
|
||||
peerDependencies:
|
||||
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':
|
||||
resolution: {integrity: sha512-NKUg54cLQSDeBWaNwAPkVIpwXtd1CrxLr0inl9Z7OdLwsidqMrncNObO6K3HgV0PEdAcqY4IwE2hkON2dlRLYw==}
|
||||
hasBin: true
|
||||
@@ -751,10 +768,18 @@ packages:
|
||||
resolution: {integrity: sha512-otHi6gAoYXKLrp8m27ZjX1PjxOPaltQ4OiUs/BhkW995mF/vXf8SWQTw68fww+Uric0v+XgoVrP9icDi+yT6zw==}
|
||||
engines: {node: '>=18.20.6'}
|
||||
|
||||
'@nuxt/kit@3.21.2':
|
||||
resolution: {integrity: sha512-Bd6m6mrDrqpBEbX+g0rc66/ALd1sxlgdx5nfK9MAYO0yKLTOSK7McSYz1KcOYn3LQFCXOWfvXwaqih/b+REI1g==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
'@nuxt/kit@4.3.1':
|
||||
resolution: {integrity: sha512-UjBFt72dnpc+83BV3OIbCT0YHLevJtgJCHpxMX0YRKWLDhhbcDdUse87GtsQBrjvOzK7WUNUYLDS/hQLYev5rA==}
|
||||
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':
|
||||
resolution: {integrity: sha512-4aNiM69Re02gI1ywnDND0m6QdVKXhWzDdtvl/16veytdHZj3FSq57ZCwOClNJ7HQkEMqXgS+bi6S2HmJX+et+g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -786,6 +811,15 @@ packages:
|
||||
'@nuxtjs/mdc@0.20.1':
|
||||
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':
|
||||
resolution: {integrity: sha512-m7TGBR2hjsBJIN9UJ909KBoKsuogo6CuLsHKvUIBXdjI0JVHP8g4ZHeB+BJpGn5LJdeSGDfz9MWiuXrZDRzunw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -1906,6 +1940,11 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
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:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -2215,6 +2254,9 @@ packages:
|
||||
cookie-es@1.2.2:
|
||||
resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==}
|
||||
|
||||
cookie-es@1.2.3:
|
||||
resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==}
|
||||
|
||||
cookie-es@2.0.0:
|
||||
resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==}
|
||||
|
||||
@@ -2372,6 +2414,9 @@ packages:
|
||||
defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
|
||||
defu@6.1.7:
|
||||
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
|
||||
|
||||
denque@2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
@@ -2587,9 +2632,25 @@ packages:
|
||||
fast-npm-meta@1.2.1:
|
||||
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:
|
||||
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:
|
||||
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
||||
|
||||
@@ -2733,6 +2794,9 @@ packages:
|
||||
resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==}
|
||||
|
||||
@@ -3338,6 +3402,9 @@ packages:
|
||||
mlly@1.8.0:
|
||||
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
|
||||
|
||||
mlly@1.8.2:
|
||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||
|
||||
mocked-exports@0.1.1:
|
||||
resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==}
|
||||
|
||||
@@ -3444,6 +3511,15 @@ packages:
|
||||
resolution: {integrity: sha512-5pVCzWXqg9HP159JDhdfQJtFvgmS/KouEVpyYLPEBXWMrQoJBwujsczmLeIKXKI2BTy4RqfXy8N1GfGTZNb57g==}
|
||||
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-umami@3.2.1:
|
||||
resolution: {integrity: sha512-82cf3kcrMn4Iq0rJ2Blfl48AqLWqRubEpxOinOoxqW7taZAd5SgZcCdCj7y4qXSt0W5DhBYgaq4IboyGFHoVUQ==}
|
||||
|
||||
nuxt@4.3.1:
|
||||
resolution: {integrity: sha512-bl+0rFcT5Ax16aiWFBFPyWcsTob19NTZaDL5P6t0MQdK63AtgS6fN6fwvwdbXtnTk6/YdCzlmuLzXhSM22h0OA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -3457,6 +3533,20 @@ packages:
|
||||
'@types/node':
|
||||
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:
|
||||
resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3552,6 +3642,10 @@ packages:
|
||||
path-browserify@1.0.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3935,6 +4029,9 @@ packages:
|
||||
remark-stringify@11.0.0:
|
||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||
|
||||
request-ip@3.3.0:
|
||||
resolution: {integrity: sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -4085,6 +4182,11 @@ packages:
|
||||
sisteransi@1.0.5:
|
||||
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:
|
||||
resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4146,6 +4248,9 @@ packages:
|
||||
std-env@3.10.0:
|
||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||
|
||||
std-env@4.0.0:
|
||||
resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==}
|
||||
|
||||
streamx@2.23.0:
|
||||
resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
|
||||
|
||||
@@ -4185,6 +4290,9 @@ packages:
|
||||
strip-literal@3.1.0:
|
||||
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
|
||||
|
||||
strnum@2.2.3:
|
||||
resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==}
|
||||
|
||||
structured-clone-es@1.0.0:
|
||||
resolution: {integrity: sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==}
|
||||
|
||||
@@ -4272,6 +4380,10 @@ packages:
|
||||
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyexec@1.1.1:
|
||||
resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -5007,12 +5119,24 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
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':
|
||||
dependencies:
|
||||
'@clack/core': 1.0.1
|
||||
picocolors: 1.1.1
|
||||
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': {}
|
||||
|
||||
'@dxup/nuxt@0.3.2(magicast@0.5.2)':
|
||||
@@ -5474,6 +5598,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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':
|
||||
dependencies:
|
||||
consola: 3.4.2
|
||||
@@ -5562,6 +5694,32 @@ snapshots:
|
||||
- magicast
|
||||
- uploadthing
|
||||
|
||||
'@nuxt/kit@3.21.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
|
||||
knitwork: 1.3.0
|
||||
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/kit@4.3.1(magicast@0.5.2)':
|
||||
dependencies:
|
||||
c12: 3.3.3(magicast@0.5.2)
|
||||
@@ -5587,6 +5745,31 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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)':
|
||||
dependencies:
|
||||
'@nuxt/devalue': 2.0.2
|
||||
@@ -5776,6 +5959,29 @@ snapshots:
|
||||
- magicast
|
||||
- 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':
|
||||
optional: true
|
||||
|
||||
@@ -6808,12 +7014,14 @@ snapshots:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
||||
acorn-import-phases@1.0.4(acorn@8.15.0):
|
||||
acorn-import-phases@1.0.4(acorn@8.16.0):
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
acorn: 8.16.0
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.18.0):
|
||||
@@ -7111,6 +7319,8 @@ snapshots:
|
||||
|
||||
cookie-es@1.2.2: {}
|
||||
|
||||
cookie-es@1.2.3: {}
|
||||
|
||||
cookie-es@2.0.0: {}
|
||||
|
||||
copy-anything@4.0.5:
|
||||
@@ -7260,6 +7470,8 @@ snapshots:
|
||||
|
||||
defu@6.1.4: {}
|
||||
|
||||
defu@6.1.7: {}
|
||||
|
||||
denque@2.1.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
@@ -7471,8 +7683,28 @@ snapshots:
|
||||
|
||||
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-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:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
@@ -7611,6 +7843,18 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
cookie-es: 1.2.2
|
||||
@@ -8500,6 +8744,13 @@ snapshots:
|
||||
pkg-types: 1.3.1
|
||||
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: {}
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
@@ -8682,6 +8933,41 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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-umami@3.2.1(magicast@0.5.2):
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
request-ip: 3.3.0
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
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:
|
||||
'@dxup/nuxt': 0.3.2(magicast@0.5.2)
|
||||
@@ -8805,6 +9091,31 @@ snapshots:
|
||||
- xml2js
|
||||
- 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:
|
||||
dependencies:
|
||||
citty: 0.2.1
|
||||
@@ -8971,6 +9282,8 @@ snapshots:
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
path-expression-matcher@1.5.0: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-key@4.0.0: {}
|
||||
@@ -9419,6 +9732,8 @@ snapshots:
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
unified: 11.0.5
|
||||
|
||||
request-ip@3.3.0: {}
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
@@ -9633,6 +9948,11 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
unicode-emoji-modifier-base: 1.0.0
|
||||
@@ -9684,6 +10004,8 @@ snapshots:
|
||||
|
||||
std-env@3.10.0: {}
|
||||
|
||||
std-env@4.0.0: {}
|
||||
|
||||
streamx@2.23.0:
|
||||
dependencies:
|
||||
events-universal: 1.0.1
|
||||
@@ -9734,6 +10056,8 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
|
||||
strnum@2.2.3: {}
|
||||
|
||||
structured-clone-es@1.0.0: {}
|
||||
|
||||
stylehacks@7.0.7(postcss@8.5.6):
|
||||
@@ -9828,6 +10152,8 @@ snapshots:
|
||||
|
||||
tinyexec@1.0.2: {}
|
||||
|
||||
tinyexec@1.1.1: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -10255,8 +10581,8 @@ snapshots:
|
||||
'@webassemblyjs/ast': 1.14.1
|
||||
'@webassemblyjs/wasm-edit': 1.14.1
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.15.0
|
||||
acorn-import-phases: 1.0.4(acorn@8.15.0)
|
||||
acorn: 8.16.0
|
||||
acorn-import-phases: 1.0.4(acorn@8.16.0)
|
||||
browserslist: 4.28.1
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.19.0
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 327 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
@@ -158,24 +158,6 @@ html, body {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Page turn transition */
|
||||
.page-slot canvas {
|
||||
transition: opacity 1s ease-out, transform 1s ease-out;
|
||||
}
|
||||
|
||||
.page-slot canvas.entering {
|
||||
opacity: 0;
|
||||
transform: translateX(var(--enter-dir, 20px));
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.page-slot canvas.leaving {
|
||||
opacity: 0;
|
||||
transform: translateX(var(--leave-dir, -20px));
|
||||
position: absolute;
|
||||
transition: opacity 1.2s ease-in, transform 1.2s ease-in;
|
||||
}
|
||||
|
||||
/* Navigation bar */
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
@@ -300,8 +282,6 @@ let currentSpread = 0; // index into spreads[]
|
||||
let spreads = []; // [[1], [2,3], [4,5], ...] — page 1 alone (cover), then pairs
|
||||
let pageCanvasCache = new Map();
|
||||
let outlinePageMap = []; // [{item, pageNum}] for highlighting
|
||||
let isAnimating = false;
|
||||
|
||||
function buildSpreads(numPages) {
|
||||
spreads = [];
|
||||
// Page 1 = couverture seule
|
||||
@@ -347,33 +327,14 @@ async function renderPageCanvas(pageNum) {
|
||||
return canvas;
|
||||
}
|
||||
|
||||
async function showSpread(index, animate = true) {
|
||||
async function showSpread(index) {
|
||||
if (index < 0 || index >= spreads.length) return;
|
||||
if (isAnimating) return;
|
||||
|
||||
const prevIndex = currentSpread;
|
||||
const direction = index > prevIndex ? 1 : -1; // 1 = forward, -1 = back
|
||||
currentSpread = index;
|
||||
|
||||
const pages = spreads[index];
|
||||
const slotLeft = document.getElementById('slotLeft');
|
||||
const slotRight = document.getElementById('slotRight');
|
||||
|
||||
// Collect old canvases for fade-out
|
||||
const oldCanvases = [...slotLeft.querySelectorAll('canvas'), ...slotRight.querySelectorAll('canvas')];
|
||||
const shouldAnimate = animate && oldCanvases.length > 0;
|
||||
|
||||
if (shouldAnimate) {
|
||||
isAnimating = true;
|
||||
// Fade out old canvases with directional slide
|
||||
oldCanvases.forEach(c => {
|
||||
c.style.setProperty('--leave-dir', `${-direction * 20}px`);
|
||||
c.classList.add('leaving');
|
||||
});
|
||||
// Wait for fade-out to mostly complete
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
slotLeft.innerHTML = '';
|
||||
slotRight.innerHTML = '';
|
||||
slotLeft.className = 'page-slot';
|
||||
@@ -381,40 +342,17 @@ async function showSpread(index, animate = true) {
|
||||
|
||||
if (pages.length === 1) {
|
||||
const canvas = await renderPageCanvas(pages[0]);
|
||||
if (shouldAnimate) {
|
||||
canvas.style.setProperty('--enter-dir', `${direction * 20}px`);
|
||||
canvas.classList.add('entering');
|
||||
}
|
||||
slotLeft.appendChild(canvas);
|
||||
slotRight.className = 'page-slot empty';
|
||||
if (shouldAnimate) {
|
||||
// Double rAF ensures the browser has painted the initial state before transitioning
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => canvas.classList.remove('entering')));
|
||||
}
|
||||
} else {
|
||||
const [left, right] = await Promise.all([
|
||||
renderPageCanvas(pages[0]),
|
||||
renderPageCanvas(pages[1]),
|
||||
]);
|
||||
if (shouldAnimate) {
|
||||
left.style.setProperty('--enter-dir', `${direction * 20}px`);
|
||||
right.style.setProperty('--enter-dir', `${direction * 20}px`);
|
||||
left.classList.add('entering');
|
||||
right.classList.add('entering');
|
||||
}
|
||||
slotLeft.appendChild(left);
|
||||
slotRight.appendChild(right);
|
||||
if (shouldAnimate) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
left.classList.remove('entering');
|
||||
right.classList.remove('entering');
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAnimate) setTimeout(() => { isAnimating = false; }, 1100);
|
||||
else isAnimating = false;
|
||||
|
||||
updateNav();
|
||||
highlightOutline();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
|
||||
// Seeds data/messages.yml from site/messages.yml on first boot (or after data loss).
|
||||
// Only runs if data/messages.yml is absent — never overwrites existing runtime data.
|
||||
export default defineNitroPlugin(async () => {
|
||||
const dataFile = join(process.cwd(), 'data', 'messages.yml')
|
||||
if (existsSync(dataFile)) return
|
||||
|
||||
const seedFile = join(process.cwd(), 'site', 'messages.yml')
|
||||
if (!existsSync(seedFile)) return
|
||||
|
||||
await mkdir(join(process.cwd(), 'data'), { recursive: true })
|
||||
const seed = await readFile(seedFile, 'utf-8')
|
||||
await writeFile(dataFile, seed, 'utf-8')
|
||||
console.log('[seed-messages] data/messages.yml initialisé depuis site/messages.yml')
|
||||
})
|
||||
@@ -3,7 +3,7 @@ book:
|
||||
author: Yvv
|
||||
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.
|
||||
coverImage: /images/book-cover.jpg
|
||||
coverImage: /images/Couv-Economie-du-don.jpg
|
||||
license: CC-BY-NC
|
||||
isbn: 979-1-042-45206-3
|
||||
songs:
|
||||
|
||||
+23
-3
@@ -1,7 +1,27 @@
|
||||
messages:
|
||||
- id: 1
|
||||
author: test
|
||||
author: Yvv
|
||||
email: ""
|
||||
text: test
|
||||
text: >-
|
||||
Bienvenue dans le librodrome. Le message que vous écrivez est le tout premier pas pour prendre contact et
|
||||
potentiellement embarquer dans la démarche. Encore un énorme boulot de préparation des outils et du fonctionnement
|
||||
de la plateforme.
|
||||
|
||||
|
||||
Ensuite le propos est d'identifier quelques bassins de vie pionniers qui se lancent dans des productions
|
||||
collectives directement, ou bien qui souhaitent passer par un événement librodrome, pour cristaliser des envies et
|
||||
passer à l'action à cette occasion.
|
||||
|
||||
|
||||
N'hésitez pas à laisser un moyen de vous répondre dans votre message ; n'hésitez pas à nommer votre bassin de vie.
|
||||
Nous prendrons contact avec vous si vous le souhaitez, nous ferons connaissance et verrons ensemble comment
|
||||
factoriser nos efforts et nos mobilisations.
|
||||
|
||||
|
||||
A bientôt.
|
||||
|
||||
Merci pour la patience en attendant l'aboutissement d'un outil opérationnel.
|
||||
type: suggestion
|
||||
published: true
|
||||
createdAt: 2026-02-20T01:23:38.633Z
|
||||
createdAt: 2026-03-19T04:09:22.881Z
|
||||
reply: null
|
||||
|
||||
+22
-1
@@ -31,7 +31,7 @@ book:
|
||||
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
|
||||
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
|
||||
cta:
|
||||
player: Présentation musicale
|
||||
@@ -49,6 +49,12 @@ axes:
|
||||
presentation:
|
||||
title: wishBounty
|
||||
text: Application pour le financement fléché des développements.
|
||||
actions:
|
||||
- id: open-dons
|
||||
label: Voir la v1 bridée (premier flux)
|
||||
icon: external-link
|
||||
highlight: true
|
||||
href: https://axiom-team.fr/dons
|
||||
- label: Authentification — WoT
|
||||
description: "Une clé pour les accès, une signature pour les actes. Le seul système sans autorité centrale ni biométrie — DID W3C, Duniter, EUDI Wallet 2026."
|
||||
to: /numerique/authentification-wot
|
||||
@@ -101,6 +107,15 @@ axes:
|
||||
to: /economique/productions-collectives
|
||||
gestation: true
|
||||
icon: users
|
||||
presentation:
|
||||
title: Ğ1flux
|
||||
text: Observatoire des transactions, premier élément d'un observatoire de l'économie monnaie-libriste.
|
||||
actions:
|
||||
- id: open-g1flux
|
||||
label: Ouvrir Ğ1flux
|
||||
icon: external-link
|
||||
highlight: true
|
||||
href: https://g1flux.syoul.fr
|
||||
politique:
|
||||
title: Autonomie citoyenne
|
||||
icon: landmark
|
||||
@@ -121,6 +136,12 @@ axes:
|
||||
to: /citoyenne/tarifs-eau
|
||||
gestation: true
|
||||
icon: droplets
|
||||
actions:
|
||||
- id: open-sejeteral0
|
||||
label: Ouvrir SejeteralO
|
||||
icon: external-link
|
||||
highlight: true
|
||||
href: https://collectivites.librodrome.org
|
||||
gratewizard:
|
||||
title: grateWizard
|
||||
subtitle: Un utilitaire pratique pour estimer les valeurs de façon relative
|
||||
|
||||
@@ -18,6 +18,11 @@ pillars:
|
||||
text: Application pour le financement fléché des développements libres.
|
||||
gestation: true
|
||||
to: /numerique/logiciel-libre
|
||||
actions:
|
||||
- label: Voir la v1 bridée (premier flux)
|
||||
icon: external-link
|
||||
highlight: true
|
||||
href: https://axiom-team.fr/dons
|
||||
|
||||
- id: authentification-wot
|
||||
label: "Authentification — WoT"
|
||||
|
||||
Reference in New Issue
Block a user