import vine from '@vinejs/vine' /** * Règles SEO + édito : * - title ≤ 60 chars (sweet spot Google snippet) * - excerpt 120-160 chars (sweet spot meta description) * - slug kebab-case ASCII unique * - tags max 5 */ const slug = () => vine .string() .minLength(3) .maxLength(200) .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/) const title = () => vine.string().minLength(5).maxLength(80) const excerpt = () => vine.string().minLength(80).maxLength(280) const tags = () => vine.array(vine.string().maxLength(50)).maxLength(5).distinct() /** * Création d'un brouillon. On accepte `status` mais c'est toujours `draft` * à la création — la publication passe par /publish. */ export const createPostValidator = vine.compile( vine.object({ slug: slug(), title: title(), excerpt: excerpt(), contentMd: vine.string().minLength(50), tags: tags().optional(), authorName: vine.string().minLength(2).maxLength(100).optional(), heroImageUrl: vine.string().url().nullable().optional(), heroImageAlt: vine.string().maxLength(250).nullable().optional(), ogImageUrl: vine.string().url().nullable().optional(), canonicalUrl: vine.string().url().nullable().optional(), noindex: vine.boolean().optional(), }), ) /** * Update partiel d'un post existant. Tous les champs optionnels. * `status` n'est pas mutable ici (passer par publish/unpublish). */ export const updatePostValidator = vine.compile( vine.object({ slug: slug().optional(), title: title().optional(), excerpt: excerpt().optional(), contentMd: vine.string().minLength(50).optional(), tags: tags().optional(), authorName: vine.string().minLength(2).maxLength(100).optional(), heroImageUrl: vine.string().url().nullable().optional(), heroImageAlt: vine.string().maxLength(250).nullable().optional(), ogImageUrl: vine.string().url().nullable().optional(), canonicalUrl: vine.string().url().nullable().optional(), noindex: vine.boolean().optional(), }), ) /** * Au publish on durcit les règles SEO inline dans AdminPostsController.publish * (cf. PUBLISH_REQUIREMENTS) — pas un validator vine séparé car on raisonne * sur l'objet Post déjà persisté (avec wordCount calculé), pas sur un payload * de requête. */ export const PUBLISH_REQUIREMENTS = { titleMaxChars: 60, // sweet spot snippet Google excerptMinChars: 120, excerptMaxChars: 160, minWordCount: 600, // ~3 min de lecture, considéré "substantiel" } as const