Générateur de portfolios PDF Figures Libres (app Nuxt autonome)

App Nuxt 3 + Nuxt UI qui lit les projets du CMS Grav et compose des portfolios
A3 paysage exportés en PDF (gabarit book_v3) : composeur WYSIWYG, templates
cover/toc/projet/grille/page libre, pipeline Playwright + Ghostscript, gel du
contenu à la génération, partage public opt-in.

Dépôt autonome : Dockerfile (base Playwright + Ghostscript + ffmpeg + rsync) inclus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 18:37:09 +02:00
commit 59dfbe4155
79 changed files with 18587 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
// Données de la vue publique : le portfolio partagé + ses projets figés.
// Public (hors mot de passe), mais uniquement si publicShare est actif et
// qu'un instantané figé existe. On ne sert jamais le contenu live.
import { getPortfolio } from '../../lib/portfolio-store'
import type { Portfolio, Project } from '~~/types'
export default defineEventHandler(async (event): Promise<{ portfolio: Portfolio; projects: Record<string, Project> }> => {
const id = getRouterParam(event, 'id') ?? ''
if (!/^[\w-]+$/.test(id)) {
throw createError({ statusCode: 400, statusMessage: 'Identifiant invalide' })
}
const portfolio = await getPortfolio(id)
if (!portfolio?.publicShare || !portfolio.frozenContent) {
throw createError({ statusCode: 404, statusMessage: 'Portfolio introuvable' })
}
return { portfolio, projects: portfolio.frozenContent.projects }
})

View File

@@ -0,0 +1,28 @@
// Médias de la vue publique : sert exclusivement le snapshot figé du portfolio
// <id>, et seulement s'il est partagé publiquement (publicShare). Aucun repli
// sur le contenu live — la surface publique reste limitée à l'instantané.
import { join } from 'node:path'
import { getPortfolio } from '../../../../lib/portfolio-store'
import { serveMedia, splitMediaPath } from '../../../../lib/media'
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id') ?? ''
if (!/^[\w-]+$/.test(id)) {
throw createError({ statusCode: 400, statusMessage: 'Identifiant invalide' })
}
const portfolio = await getPortfolio(id)
if (!portfolio?.publicShare) {
throw createError({ statusCode: 404, statusMessage: 'Portfolio introuvable' })
}
const { projectId, filename } = splitMediaPath(getRouterParam(event, 'path') ?? '')
const query = getQuery(event)
const width = Number(query.w) || 0
const wantPoster = Boolean(query.poster)
const config = useRuntimeConfig(event)
const roots = [join(config.dataDir, 'frozen', id)]
return serveMedia(event, { roots, projectId, filename, width, wantPoster, dataDir: config.dataDir })
})