import { notFound } from 'next/navigation' import { getPublishedNote } from '@/app/actions/notes-publishing' import { Flag, Sparkles } from 'lucide-react' import { format } from 'date-fns' import { fr } from 'date-fns/locale' import { computePublishedSourceHash } from '@/lib/publish/template-render' import { processNoteHtmlForPublish } from '@/lib/publish/process-note-html' import { REWRITE_SHARED_CSS, KATEX_PUBLISH_CSS } from '@/lib/publish/shared-css' import { ReadingProgress } from '@/components/publish/reading-progress' import { CopyLinkButton } from '@/components/publish/copy-link-button' export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params const note = await getPublishedNote(slug) if (!note) return { title: 'Note introuvable — Memento' } return { title: `${note.title || 'Note publiée'} — Memento`, description: note.content?.replace(/<[^>]+>/g, '').slice(0, 160), } } export const revalidate = 60 function estimateReadingTime(html: string): number { const words = html.replace(/<[^>]+>/g, ' ').trim().split(/\s+/).filter(Boolean).length return Math.max(1, Math.ceil(words / 200)) } function formatDate(d: Date | string | null | undefined, pattern = 'd MMMM yyyy') { if (!d) return null return format(new Date(d), pattern, { locale: fr }) } /* ─── Shared base CSS (selection, print, typography polish) ─────────────── */ const BASE_RESET = ` *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; } img { max-width: 100%; height: auto; } ::selection { background: color-mix(in srgb, var(--pub-accent, #A47148) 28%, transparent); } @media print { .pub-nav, .pub-progress, .pub-copy, .pub-report { display: none !important; } .pub-root { min-height: auto !important; } } ` const ARTICLE_TYPO = ` .pub-article img { max-width: 100%; height: auto; } .pub-article h2 { scroll-margin-top: 5rem; } .pub-article h3 { scroll-margin-top: 5rem; } .pub-article p + p { margin-top: 0; } .pub-article .pub-body-source > p:first-of-type::first-letter, .pub-article .pub-rewrite-body > p:first-of-type::first-letter { font-weight: 700; } .pub-article mark { background: color-mix(in srgb, var(--pub-accent, #A47148) 22%, transparent); padding: 0.05em 0.2em; border-radius: 2px; } .pub-article hr { border: none; border-top: 1px solid color-mix(in srgb, var(--pub-accent, #A47148) 25%, transparent); margin: 2.5em auto; width: 40%; } .pub-article .pub-gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 2.5em; } .pub-article .pub-gallery-img { width: 100%; height: 200px; object-fit: cover; border-radius: 8px; display: block; } .pub-article .pub-figure { margin: 2.25em 0; text-align: center; } .pub-article .pub-figure-img { max-width: 100%; border-radius: 10px; box-shadow: 0 16px 48px rgba(0,0,0,0.1); } .pub-article .pub-figure-caption { margin-top: 0.7em; font-size: 0.82em; opacity: 0.65; font-style: italic; } ` /* ─── MAGAZINE — editorial prestige ─────────────────────────────────────── */ function MagazinePage({ note, bodyHtml, readingTime, slug, isStale }: PageProps) { const dateLabel = formatDate(note.publishedAt) return ( <>
Article {dateLabel && <>{dateLabel}} {readingTime} min de lecture

{note.title || 'Sans titre'}

{note.user?.name && (
{note.user.image && }
{note.user.name} Publié sur Memento
)}
Memento

Votre mémoire augmentée par l’IA

{isStale && (

Le contenu source a évolué depuis la dernière publication.

)}
) } /* ─── BRIEF — crisp knowledge card ──────────────────────────────────────── */ function BriefPage({ note, bodyHtml, readingTime, slug, isStale }: PageProps) { const dateLabel = formatDate(note.publishedAt, 'd MMM yyyy') return ( <>
Fiche expert {readingTime} min {dateLabel && {dateLabel}}

{note.title || 'Sans titre'}

{note.user?.name && (
{note.user.image && } {note.user.name}
)}
Memento — Fiche structurée pour aller à l’essentiel {isStale &&

Le contenu source a évolué depuis la publication.

}
) } /* ─── ESSAY — literary calm ─────────────────────────────────────────────── */ function EssayPage({ note, bodyHtml, readingTime, slug, isStale }: PageProps) { const dateLabel = formatDate(note.publishedAt) return ( <>
{dateLabel && }

{note.title || 'Sans titre'}

{note.user?.name && (
{note.user.image && } {note.user.name}
)} {readingTime} min de lecture

Publié sur Memento

{isStale &&

Le contenu source a évolué depuis la publication.

}
) } /* ─── SIMPLE — clean Substack-grade default ─────────────────────────────── */ function SimplePage({ note, bodyHtml, readingTime, slug, isStale }: PageProps) { const dateLabel = formatDate(note.publishedAt, 'd MMM yyyy') return ( <>
{dateLabel && {dateLabel}} {dateLabel && } {readingTime} min de lecture

{note.title || 'Sans titre'}

{note.user?.name && (
{note.user.image && } par {note.user.name}
)}
Memento — Votre mémoire augmentée par l’IA {isStale && (

Le contenu source a évolué depuis la publication.

)}
) } /* ─── Types ─────────────────────────────────────────────────────────────── */ type NoteData = NonNullable>> interface PageProps { note: NoteData bodyHtml: string readingTime: number slug: string isStale: boolean } /* ─── Route ──────────────────────────────────────────────────────────────── */ export default async function PublishedNotePage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params const note = await getPublishedNote(slug) if (!note) notFound() const usesAiLayout = Boolean(note.publishedContent) const rawHtml = usesAiLayout ? note.publishedContent! : (note.content || '') const bodyHtml = processNoteHtmlForPublish(rawHtml) const readingSource = usesAiLayout ? note.publishedContent! : (note.content || '') const readingTime = estimateReadingTime(readingSource) const isStale = Boolean( note.publishedSourceHash && computePublishedSourceHash(note.content || '') !== note.publishedSourceHash ) const props: PageProps = { note, bodyHtml, readingTime, slug, isStale } if (!usesAiLayout) return if (note.publishedTemplate === 'brief') return if (note.publishedTemplate === 'essay') return return }