diff --git a/apps/api/app/transformers/post_transformer.ts b/apps/api/app/transformers/post_transformer.ts index 72f2a90..1687e2d 100644 --- a/apps/api/app/transformers/post_transformer.ts +++ b/apps/api/app/transformers/post_transformer.ts @@ -2,12 +2,14 @@ import type Post from '#models/post' import { BaseTransformer } from '@adonisjs/core/transformers' /** - * PostTransformer — sérialise un Post en JSON public pour l'app Astro qui - * rend les pages blog en SSR (cf. apps/landing/src/pages/blog/*.astro). + * PostTransformer — sérialise un Post en JSON pour les consommateurs + * (Astro landing en SSR public, et apps/web admin en édition). * - * On ne retourne JAMAIS contentMd (c'est la source d'édition) — uniquement - * contentHtml déjà rendu par blog_renderer côté API. Garde le payload léger - * et évite de re-parser markdown côté front. + * On expose à la fois `contentMd` (source markdown — utile pour l'éditeur + * admin) et `contentHtml` (déjà rendu — utilisé par la landing). Le surcoût + * en bytes côté landing est marginal (~quelques KB/article) et la + * simplicité d'avoir un seul transformer prévaut. Si le volume devient + * problématique, on splittera en PublicPostTransformer + AdminPostTransformer. */ export default class PostTransformer extends BaseTransformer { toObject() { @@ -17,7 +19,9 @@ export default class PostTransformer extends BaseTransformer { slug: p.slug, title: p.title, excerpt: p.excerpt, + contentMd: p.contentMd, contentHtml: p.contentHtml, + status: p.status, heroImageUrl: p.heroImageUrl, heroImageAlt: p.heroImageAlt, ogImageUrl: p.ogImageUrl, @@ -25,6 +29,7 @@ export default class PostTransformer extends BaseTransformer { authorName: p.authorName, tags: p.tags, publishedAt: p.publishedAt?.toISO() ?? null, + createdAt: p.createdAt.toISO()!, updatedAt: p.updatedAt?.toISO() ?? null, readingTimeMinutes: p.readingTimeMinutes, wordCount: p.wordCount, diff --git a/apps/web/src/routes/_app/admin.blog_.$id.tsx b/apps/web/src/routes/_app/admin.blog_.$id.tsx index 633c08b..76ca042 100644 --- a/apps/web/src/routes/_app/admin.blog_.$id.tsx +++ b/apps/web/src/routes/_app/admin.blog_.$id.tsx @@ -118,25 +118,25 @@ function Editor({ post, onBack }: { post: AdminPost; onBack: () => void }) { // État local — on travaille sur une copie du post pour autosave debouncée. const [draft, setDraft] = useState({ - slug: post.slug, - title: post.title, - excerpt: post.excerpt, - contentMd: post.contentMd, + slug: post.slug ?? "", + title: post.title ?? "", + excerpt: post.excerpt ?? "", + contentMd: post.contentMd ?? "", heroImageUrl: post.heroImageUrl, heroImageAlt: post.heroImageAlt, - tags: post.tags.join(", "), + tags: (post.tags ?? []).join(", "), }); // Re-sync si le post est rafraîchi par un autre canal (rare mais sûr). useEffect(() => { setDraft({ - slug: post.slug, - title: post.title, - excerpt: post.excerpt, - contentMd: post.contentMd, + slug: post.slug ?? "", + title: post.title ?? "", + excerpt: post.excerpt ?? "", + contentMd: post.contentMd ?? "", heroImageUrl: post.heroImageUrl, heroImageAlt: post.heroImageAlt, - tags: post.tags.join(", "), + tags: (post.tags ?? []).join(", "), }); }, [post.id]); // eslint-disable-line react-hooks/exhaustive-deps @@ -510,7 +510,8 @@ function CharCounter({ value, max, label }: { value: number; max: number; label: } /** Compte les mots côté client — heuristique simple, le serveur a la source de vérité. */ -function countWords(md: string): number { +function countWords(md: string | null | undefined): number { + if (!md) return 0; const stripped = md .replace(/```[\s\S]*?```/g, " ") .replace(/`[^`]*`/g, " ")