feat: SEO complet + analytics Umami + og:image § logo
ci/woodpecker/push/woodpecker Pipeline was successful
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user