fix(blog/admin): expose contentMd dans PostTransformer + nullish guards
Le PostTransformer ne renvoyait que contentHtml — l'éditeur admin avait besoin du contentMd source pour permettre l'édition, et plantait avec "Cannot read properties of undefined (reading 'replace')" dans countWords() au mount. * PostTransformer expose maintenant contentMd, status et createdAt en plus de l'existant. Surcoût ~quelques KB par requête côté landing publique (négligeable). Si volume devient un problème, on splittera en PublicPostTransformer + AdminPostTransformer. * admin.blog_.$id.tsx : nullish coalescing sur tous les champs string au moment d'init le draft (defense in depth — si l'API renvoie jamais un payload partiel, l'éditeur reste fonctionnel). * countWords() accepte maintenant string | null | undefined. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6dcae6956c
commit
52bc7507fb
@ -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<Post> {
|
||||
toObject() {
|
||||
@ -17,7 +19,9 @@ export default class PostTransformer extends BaseTransformer<Post> {
|
||||
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<Post> {
|
||||
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,
|
||||
|
||||
@ -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, " ")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user