Un pot de crédits global (BASIC 100 / PRO 1 000 / BUSINESS 4 000) remplace les seaux par fonction côté débit. Packs one-shot S/M/L, admin sans seaux legacy, usage multi-features, hints de coût, anti-flash thème et hydratation admin. Inclut aussi le pipeline slides renforcé et la page publique polishée.
1096 lines
46 KiB
TypeScript
1096 lines
46 KiB
TypeScript
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 (
|
|
<>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
|
|
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700;9..144,900&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;1,8..60,400&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
<ReadingProgress accent="#C9A227" />
|
|
|
|
<div className="mag-root pub-root" data-pub-root>
|
|
<nav className="mag-nav pub-nav">
|
|
<span className="mag-nav-brand">Memento</span>
|
|
<div className="mag-nav-right">
|
|
<CopyLinkButton
|
|
className="mag-copy pub-copy"
|
|
label="Copier le lien"
|
|
copiedLabel="Copié"
|
|
/>
|
|
<span className="mag-badge"><Sparkles size={10} /> Magazine</span>
|
|
<a href={`/p/${slug}/report`} className="mag-report pub-report"><Flag size={12} /> Signaler</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<header className="mag-header">
|
|
<div className="mag-header-glow" aria-hidden />
|
|
<div className="mag-header-inner">
|
|
<div className="mag-kicker">
|
|
<span>Article</span>
|
|
{dateLabel && <><span className="mag-dot" />{dateLabel}</>}
|
|
<span className="mag-dot" />
|
|
<span>{readingTime} min de lecture</span>
|
|
</div>
|
|
<h1 className="mag-title">{note.title || 'Sans titre'}</h1>
|
|
{note.user?.name && (
|
|
<div className="mag-author">
|
|
{note.user.image && <img src={note.user.image} alt="" className="mag-avatar" />}
|
|
<div>
|
|
<span className="mag-author-name">{note.user.name}</span>
|
|
<span className="mag-author-role">Publié sur Memento</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
<div className="mag-content">
|
|
<div dir="auto" className="mag-body pub-article" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
|
|
</div>
|
|
|
|
<footer className="mag-footer">
|
|
<div className="mag-footer-inner">
|
|
<div className="mag-footer-brand">Memento</div>
|
|
<p className="mag-footer-tag">Votre mémoire augmentée par l’IA</p>
|
|
{isStale && (
|
|
<p className="mag-stale">Le contenu source a évolué depuis la dernière publication.</p>
|
|
)}
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
|
|
<style>{`
|
|
${BASE_RESET}
|
|
${ARTICLE_TYPO}
|
|
.mag-root {
|
|
--pub-accent: #C9A227;
|
|
min-height: 100vh;
|
|
background: #faf9f6;
|
|
color: #141414;
|
|
font-family: 'Inter', system-ui, sans-serif;
|
|
}
|
|
|
|
.mag-nav {
|
|
position: sticky; top: 0; z-index: 30;
|
|
background: rgba(12,12,12,0.92);
|
|
backdrop-filter: blur(16px) saturate(1.2);
|
|
color: #fff;
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 0 28px; height: 54px;
|
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
}
|
|
.mag-nav-brand {
|
|
font-family: 'Fraunces', Georgia, serif;
|
|
font-size: 18px; font-weight: 900; letter-spacing: 0.02em;
|
|
}
|
|
.mag-nav-right { display: flex; align-items: center; gap: 10px; }
|
|
.mag-badge {
|
|
display: none;
|
|
align-items: center; gap: 5px;
|
|
font-size: 10px; font-weight: 600; letter-spacing: 0.14em;
|
|
text-transform: uppercase; color: #e8c96a;
|
|
}
|
|
@media (min-width: 640px) { .mag-badge { display: flex; } }
|
|
.mag-copy {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
font-size: 11px; font-weight: 500; color: rgba(255,255,255,0.65);
|
|
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);
|
|
border-radius: 999px; padding: 5px 12px; cursor: pointer;
|
|
font-family: inherit; transition: color .15s, background .15s;
|
|
}
|
|
.mag-copy:hover { color: #fff; background: rgba(255,255,255,0.12); }
|
|
.mag-report {
|
|
display: flex; align-items: center; gap: 5px;
|
|
font-size: 11px; color: rgba(255,255,255,0.4); text-decoration: none;
|
|
transition: color .15s;
|
|
}
|
|
.mag-report:hover { color: #fff; }
|
|
|
|
.mag-header {
|
|
position: relative;
|
|
background: #0c0c0c;
|
|
color: #fff;
|
|
padding: 72px 28px 88px;
|
|
text-align: center;
|
|
overflow: hidden;
|
|
}
|
|
.mag-header-glow {
|
|
position: absolute; inset: 0; pointer-events: none;
|
|
background:
|
|
radial-gradient(ellipse 80% 60% at 50% 0%, rgba(201,162,39,0.18), transparent 60%),
|
|
radial-gradient(ellipse 50% 40% at 80% 100%, rgba(164,113,72,0.12), transparent 50%);
|
|
}
|
|
.mag-header-inner { position: relative; max-width: 820px; margin: 0 auto; }
|
|
.mag-kicker {
|
|
display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px;
|
|
font-size: 11px; font-weight: 600; letter-spacing: 0.18em;
|
|
text-transform: uppercase; color: rgba(255,255,255,0.5); margin-bottom: 28px;
|
|
}
|
|
.mag-kicker span:first-child { color: #e8c96a; }
|
|
.mag-dot { width: 3px; height: 3px; border-radius: 50%; background: rgba(255,255,255,0.25); }
|
|
.mag-title {
|
|
font-family: 'Fraunces', Georgia, serif;
|
|
font-size: clamp(2.1rem, 6.5vw, 4.25rem);
|
|
font-weight: 900; line-height: 1.05; letter-spacing: -0.03em;
|
|
margin-bottom: 36px;
|
|
text-wrap: balance;
|
|
}
|
|
.mag-author {
|
|
display: inline-flex; align-items: center; gap: 12px;
|
|
text-align: left;
|
|
}
|
|
.mag-avatar {
|
|
width: 44px; height: 44px; border-radius: 50%;
|
|
border: 2px solid rgba(232,201,106,0.45);
|
|
object-fit: cover;
|
|
}
|
|
.mag-author-name { display: block; font-size: 14px; font-weight: 600; color: #fff; }
|
|
.mag-author-role { display: block; font-size: 12px; color: rgba(255,255,255,0.45); margin-top: 2px; }
|
|
|
|
.mag-content {
|
|
max-width: 720px; margin: 0 auto;
|
|
padding: 56px 24px 96px;
|
|
}
|
|
@media (min-width: 900px) {
|
|
.mag-content { padding-left: 32px; padding-right: 32px; }
|
|
}
|
|
|
|
.mag-body {
|
|
font-family: 'Source Serif 4', Georgia, serif;
|
|
font-size: 19px; line-height: 1.85; color: #1a1a1a;
|
|
font-optical-sizing: auto;
|
|
}
|
|
.mag-body .pub-tpl { all: unset; display: block; }
|
|
|
|
.mag-body .pub-magazine-dek {
|
|
font-size: 1.28em; line-height: 1.55; color: #333;
|
|
margin-bottom: 2.25em; padding-bottom: 1.75em;
|
|
border-bottom: 2px solid #0c0c0c;
|
|
font-style: italic;
|
|
text-wrap: pretty;
|
|
}
|
|
.mag-body .pub-pull-quote {
|
|
font-family: 'Fraunces', serif;
|
|
font-size: clamp(1.45em, 3.5vw, 1.9em);
|
|
font-style: italic; font-weight: 700;
|
|
color: #0c0c0c; line-height: 1.3;
|
|
margin: 2.75em 0; padding: 1.4em 0;
|
|
border-top: 3px solid #C9A227;
|
|
border-bottom: 3px solid #C9A227;
|
|
text-align: center; text-wrap: balance;
|
|
}
|
|
.mag-body .pub-pull-quote p { margin: 0; }
|
|
|
|
.mag-body .pub-hero {
|
|
margin: -8px -8px 2.75rem;
|
|
border-radius: 4px; overflow: hidden;
|
|
max-height: 520px; aspect-ratio: 16/9; background: #1a1a1a;
|
|
box-shadow: 0 24px 64px rgba(0,0,0,0.12);
|
|
}
|
|
@media (min-width: 900px) {
|
|
.mag-body .pub-hero { margin-left: -48px; margin-right: -48px; border-radius: 6px; }
|
|
}
|
|
.mag-body .pub-hero-img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
|
|
.mag-body h2 {
|
|
font-family: 'Fraunces', serif; font-size: 1.55em; font-weight: 700;
|
|
margin-top: 2.6em; margin-bottom: 0.55em; letter-spacing: -0.02em; color: #0c0c0c;
|
|
}
|
|
.mag-body h3 { font-size: 1.18em; font-weight: 600; margin-top: 2em; margin-bottom: 0.45em; }
|
|
.mag-body p { margin: 1.05em 0; text-wrap: pretty; }
|
|
.mag-body .pub-body-source > p:first-of-type::first-letter {
|
|
font-family: 'Fraunces', serif;
|
|
float: left; font-size: 3.4em; line-height: 0.85;
|
|
padding-right: 0.1em; padding-top: 0.08em;
|
|
font-weight: 900; color: #0c0c0c;
|
|
}
|
|
.mag-body blockquote {
|
|
border-left: 3px solid #C9A227; padding-left: 1.25em;
|
|
margin: 1.75em 0; color: #444; font-style: italic;
|
|
}
|
|
.mag-body ul, .mag-body ol { padding-left: 1.4em; margin: 1.1em 0; }
|
|
.mag-body li { margin: 0.4em 0; }
|
|
.mag-body pre {
|
|
background: #121212; color: #e8e3d5; padding: 22px;
|
|
border-radius: 10px; overflow-x: auto; font-size: 13.5px; margin: 1.75em 0;
|
|
}
|
|
.mag-body code {
|
|
font-family: 'SF Mono', Menlo, monospace; font-size: 0.88em;
|
|
background: #f0ebe3; padding: 1px 6px; border-radius: 4px;
|
|
}
|
|
.mag-body pre code { background: none; padding: 0; color: inherit; }
|
|
.mag-body table { border-collapse: collapse; width: 100%; margin: 1.75em 0; }
|
|
.mag-body th {
|
|
background: #0c0c0c; color: #fff; padding: 11px 14px;
|
|
text-align: left; font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase;
|
|
}
|
|
.mag-body td { border-bottom: 1px solid #e8e4dc; padding: 11px 14px; }
|
|
.mag-body a {
|
|
color: #9a7212; text-decoration: underline;
|
|
text-underline-offset: 3px; text-decoration-thickness: 1px;
|
|
}
|
|
.mag-body a:hover { color: #6b4f0c; }
|
|
.mag-body .pub-callout { border-radius: 8px; padding: 14px 18px; margin: 1.5em 0; }
|
|
${KATEX_PUBLISH_CSS}
|
|
|
|
.mag-footer {
|
|
background: #0c0c0c; color: rgba(255,255,255,0.4);
|
|
padding: 48px 28px 56px; text-align: center;
|
|
}
|
|
.mag-footer-inner { max-width: 480px; margin: 0 auto; }
|
|
.mag-footer-brand {
|
|
font-family: 'Fraunces', serif; font-size: 20px; font-weight: 900;
|
|
color: #e8c96a; margin-bottom: 8px;
|
|
}
|
|
.mag-footer-tag { font-size: 13px; line-height: 1.5; }
|
|
.mag-stale { margin-top: 14px; font-size: 11px; color: rgba(255,255,255,0.28); }
|
|
|
|
.mag-body {
|
|
--pub-accent: #C9A227;
|
|
--pub-summary-color: #333;
|
|
--pub-exercise-border: #e0d4c3;
|
|
--pub-exercise-header-bg: #faf6ee;
|
|
--pub-solution-bg: #faf6ee;
|
|
--pub-definition-border: #e0d4c3;
|
|
--pub-definition-bg: #fafaf8;
|
|
--pub-toggle-border: #e5e5e5;
|
|
--pub-toggle-header-bg: #f7f7f5;
|
|
--pub-highlight-bg: #faf6ee;
|
|
--pub-checklist-bg: rgba(0,0,0,0.03);
|
|
}
|
|
.mag-body .pub-rewrite-body .pub-code { background: #0d0d0d; }
|
|
.mag-body .pub-rewrite-body .pub-code code { background: #0d0d0d; color: #e8e3d5; }
|
|
.mag-body .pub-rewrite-body .pub-definition-term { background: #e8c96a; color: #1a1a1a; }
|
|
.mag-body .pub-rewrite-body .pub-step-num { background: #9a7212; color: #fff; }
|
|
.mag-body .pub-rewrite-body .pub-exercise-num { color: #9a7212; }
|
|
.mag-body .pub-rewrite-body .pub-solution > summary { color: #9a7212; }
|
|
.mag-body .pub-rewrite-body .pub-checklist li::before { color: #9a7212; }
|
|
${REWRITE_SHARED_CSS}
|
|
`}</style>
|
|
</>
|
|
)
|
|
}
|
|
|
|
/* ─── BRIEF — crisp knowledge card ──────────────────────────────────────── */
|
|
function BriefPage({ note, bodyHtml, readingTime, slug, isStale }: PageProps) {
|
|
const dateLabel = formatDate(note.publishedAt, 'd MMM yyyy')
|
|
return (
|
|
<>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
<ReadingProgress accent="#3B82F6" />
|
|
|
|
<div className="br-root pub-root" data-pub-root>
|
|
<nav className="br-nav pub-nav">
|
|
<span className="br-brand">Memento</span>
|
|
<div className="br-nav-right">
|
|
<CopyLinkButton className="br-copy pub-copy" label="Copier le lien" copiedLabel="Copié" />
|
|
<span className="br-badge"><Sparkles size={9} /> Fiche expert</span>
|
|
<a href={`/p/${slug}/report`} className="br-report pub-report" aria-label="Signaler"><Flag size={12} /></a>
|
|
</div>
|
|
</nav>
|
|
|
|
<header className="br-header">
|
|
<div className="br-header-mesh" aria-hidden />
|
|
<div className="br-header-inner">
|
|
<div className="br-meta">
|
|
<span className="br-tag">Fiche expert</span>
|
|
<span className="br-meta-item">{readingTime} min</span>
|
|
{dateLabel && <span className="br-meta-item">{dateLabel}</span>}
|
|
</div>
|
|
<h1 className="br-title">{note.title || 'Sans titre'}</h1>
|
|
{note.user?.name && (
|
|
<div className="br-author">
|
|
{note.user.image && <img src={note.user.image} alt="" className="br-avatar" />}
|
|
<span>{note.user.name}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
<div className="br-layout">
|
|
<div dir="auto" className="br-body pub-article" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
|
|
</div>
|
|
|
|
<footer className="br-footer">
|
|
<strong>Memento</strong>
|
|
<span> — Fiche structurée pour aller à l’essentiel</span>
|
|
{isStale && <p className="br-stale">Le contenu source a évolué depuis la publication.</p>}
|
|
</footer>
|
|
</div>
|
|
|
|
<style>{`
|
|
${BASE_RESET}
|
|
${ARTICLE_TYPO}
|
|
.br-root {
|
|
--pub-accent: #3B82F6;
|
|
min-height: 100vh;
|
|
background: linear-gradient(180deg, #eef2f8 0%, #f6f8fb 40%, #f6f8fb 100%);
|
|
color: #0f172a;
|
|
font-family: 'DM Sans', system-ui, sans-serif;
|
|
}
|
|
|
|
.br-nav {
|
|
position: sticky; top: 0; z-index: 30;
|
|
background: rgba(255,255,255,0.85);
|
|
backdrop-filter: blur(16px) saturate(1.2);
|
|
border-bottom: 1px solid rgba(15,23,42,0.06);
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 0 24px; height: 52px;
|
|
}
|
|
.br-brand { font-weight: 700; font-size: 15px; letter-spacing: -0.02em; color: #0f172a; }
|
|
.br-nav-right { display: flex; align-items: center; gap: 10px; }
|
|
.br-badge {
|
|
display: none; align-items: center; gap: 4px;
|
|
font-size: 11px; font-weight: 600; color: #2563eb;
|
|
background: #eff6ff; padding: 4px 10px; border-radius: 999px;
|
|
}
|
|
@media (min-width: 640px) { .br-badge { display: flex; } }
|
|
.br-copy {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
font-size: 11px; font-weight: 500; color: #475569;
|
|
background: #f1f5f9; border: 1px solid #e2e8f0;
|
|
border-radius: 999px; padding: 5px 12px; cursor: pointer;
|
|
font-family: inherit; transition: background .15s;
|
|
}
|
|
.br-copy:hover { background: #e2e8f0; }
|
|
.br-report { color: #94a3b8; display: flex; padding: 6px; border-radius: 8px; transition: color .15s, background .15s; }
|
|
.br-report:hover { color: #475569; background: #f1f5f9; }
|
|
|
|
.br-header {
|
|
position: relative;
|
|
background: linear-gradient(145deg, #0b1f3a 0%, #123056 45%, #1e3a5f 100%);
|
|
color: #fff; padding: 52px 24px 64px; overflow: hidden;
|
|
}
|
|
.br-header-mesh {
|
|
position: absolute; inset: 0; pointer-events: none; opacity: 0.45;
|
|
background-image:
|
|
radial-gradient(circle at 20% 20%, rgba(59,130,246,0.35), transparent 40%),
|
|
radial-gradient(circle at 85% 70%, rgba(14,165,233,0.2), transparent 35%),
|
|
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
|
|
background-size: auto, auto, 32px 32px, 32px 32px;
|
|
}
|
|
.br-header-inner { position: relative; max-width: 760px; margin: 0 auto; }
|
|
.br-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 18px; }
|
|
.br-tag {
|
|
background: #3B82F6; color: #fff;
|
|
font-size: 10px; font-weight: 700; letter-spacing: 0.12em;
|
|
text-transform: uppercase; padding: 4px 10px; border-radius: 6px;
|
|
}
|
|
.br-meta-item {
|
|
font-size: 12px; font-weight: 500;
|
|
color: rgba(255,255,255,0.55); letter-spacing: 0.04em;
|
|
}
|
|
.br-title {
|
|
font-size: clamp(1.6rem, 4.2vw, 2.65rem);
|
|
font-weight: 700; line-height: 1.15; letter-spacing: -0.03em;
|
|
margin-bottom: 22px; text-wrap: balance;
|
|
}
|
|
.br-author {
|
|
display: flex; align-items: center; gap: 10px;
|
|
font-size: 13px; color: rgba(255,255,255,0.55);
|
|
}
|
|
.br-avatar {
|
|
width: 30px; height: 30px; border-radius: 50%;
|
|
border: 2px solid rgba(255,255,255,0.2); object-fit: cover;
|
|
}
|
|
|
|
.br-layout {
|
|
max-width: 760px; margin: -28px auto 0;
|
|
padding: 0 20px 80px; position: relative; z-index: 1;
|
|
}
|
|
|
|
.br-body { font-size: 16.5px; line-height: 1.75; color: #1e293b; }
|
|
.br-body .pub-tpl { all: unset; display: block; }
|
|
|
|
.br-body .pub-brief-lead {
|
|
background: #fff; border-radius: 16px;
|
|
border: 1px solid rgba(15,23,42,0.06);
|
|
border-left: 4px solid #3B82F6;
|
|
padding: 22px 26px; margin-bottom: 1.25em;
|
|
box-shadow: 0 12px 40px rgba(15,23,42,0.06);
|
|
}
|
|
.br-body .pub-brief-lead p {
|
|
margin: 0; font-size: 1.06em; line-height: 1.7;
|
|
color: #0f172a; font-weight: 500;
|
|
}
|
|
|
|
.br-body .pub-key-points {
|
|
background: #fff; border-radius: 16px; padding: 22px 26px;
|
|
margin-bottom: 1.25em;
|
|
border: 1px solid rgba(15,23,42,0.06);
|
|
box-shadow: 0 12px 40px rgba(15,23,42,0.05);
|
|
}
|
|
.br-body .pub-key-points-label {
|
|
font-size: 10px; text-transform: uppercase; letter-spacing: 0.18em;
|
|
font-weight: 700; color: #2563eb; margin-bottom: 14px; display: block;
|
|
}
|
|
.br-body .pub-key-points ul { list-style: none; padding: 0; margin: 0; counter-reset: kp; }
|
|
.br-body .pub-key-points li {
|
|
counter-increment: kp;
|
|
padding: 12px 14px 12px 48px; border-radius: 12px;
|
|
background: #f8fafc; margin-bottom: 8px; position: relative;
|
|
font-size: 0.95em; font-weight: 500; border: 1px solid #eef2f7;
|
|
}
|
|
.br-body .pub-key-points li::before {
|
|
content: counter(kp);
|
|
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
|
|
width: 26px; height: 26px; border-radius: 8px;
|
|
background: #eff6ff; color: #2563eb;
|
|
font-weight: 700; font-size: 12px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
|
|
.br-body .pub-body-source,
|
|
.br-body .pub-rewrite-body {
|
|
background: #fff; border-radius: 16px; padding: 32px 28px;
|
|
border: 1px solid rgba(15,23,42,0.06);
|
|
box-shadow: 0 12px 40px rgba(15,23,42,0.05);
|
|
}
|
|
@media (min-width: 640px) {
|
|
.br-body .pub-body-source,
|
|
.br-body .pub-rewrite-body { padding: 36px 40px; }
|
|
}
|
|
|
|
.br-body .pub-hero {
|
|
margin: 0 0 1.25rem; border-radius: 16px; overflow: hidden;
|
|
max-height: 380px; aspect-ratio: 16/9; background: #dbe3ef;
|
|
box-shadow: 0 16px 48px rgba(15,23,42,0.1);
|
|
}
|
|
.br-body .pub-hero-img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
|
|
.br-body h2 {
|
|
font-size: 1.22em; font-weight: 700; margin-top: 1.9em; margin-bottom: 0.55em;
|
|
color: #0f172a; letter-spacing: -0.02em;
|
|
padding-bottom: 0.4em; border-bottom: 1px solid #e2e8f0;
|
|
}
|
|
.br-body h3 { font-size: 1.06em; font-weight: 600; margin-top: 1.4em; margin-bottom: 0.4em; color: #1e3a5f; }
|
|
.br-body p { margin: 0.85em 0; text-wrap: pretty; }
|
|
.br-body blockquote {
|
|
border-left: 3px solid #3B82F6; padding: 4px 0 4px 1em;
|
|
margin: 1.2em 0; color: #334155; font-style: italic;
|
|
}
|
|
.br-body ul, .br-body ol { padding-left: 1.3em; margin: 1em 0; }
|
|
.br-body li { margin: 0.35em 0; }
|
|
.br-body pre {
|
|
background: #0f172a; color: #e2e8f0; padding: 20px;
|
|
border-radius: 12px; overflow-x: auto; font-size: 13px;
|
|
margin: 1.4em 0; font-family: 'DM Mono', monospace;
|
|
}
|
|
.br-body code {
|
|
font-family: 'DM Mono', monospace; font-size: 0.87em;
|
|
background: #eef2f7; padding: 1px 6px; border-radius: 4px; color: #1e3a5f;
|
|
}
|
|
.br-body pre code { background: none; color: inherit; padding: 0; }
|
|
.br-body table { border-collapse: collapse; width: 100%; margin: 1.4em 0; border-radius: 10px; overflow: hidden; }
|
|
.br-body th { background: #0f172a; color: #fff; padding: 10px 14px; text-align: left; font-size: 12px; }
|
|
.br-body td { border-bottom: 1px solid #e2e8f0; padding: 10px 14px; background: #fff; }
|
|
.br-body a { color: #2563eb; text-decoration: underline; text-underline-offset: 3px; }
|
|
.br-body .pub-callout { border-radius: 10px; padding: 12px 16px; margin: 1.2em 0; }
|
|
${KATEX_PUBLISH_CSS}
|
|
|
|
.br-footer {
|
|
background: #0f172a; color: rgba(255,255,255,0.4);
|
|
padding: 36px 24px; text-align: center; font-size: 13px;
|
|
}
|
|
.br-footer strong { color: #fff; font-weight: 600; }
|
|
.br-stale { font-size: 11px; color: rgba(255,255,255,0.28); margin-top: 8px; }
|
|
|
|
.br-body {
|
|
--pub-accent: #2563eb;
|
|
--pub-summary-color: #334155;
|
|
--pub-exercise-border: #bfdbfe;
|
|
--pub-exercise-header-bg: #eff6ff;
|
|
--pub-solution-bg: #f0f7ff;
|
|
--pub-definition-border: #bfdbfe;
|
|
--pub-definition-bg: #f8faff;
|
|
--pub-toggle-border: #e2e8f0;
|
|
--pub-toggle-header-bg: #f8fafc;
|
|
--pub-highlight-bg: #eff6ff;
|
|
--pub-checklist-bg: #f4f6f9;
|
|
}
|
|
.br-body .pub-rewrite-body .pub-code { background: #0f172a; }
|
|
.br-body .pub-rewrite-body .pub-code code { background: #0f172a; color: #e2e8f0; }
|
|
${REWRITE_SHARED_CSS}
|
|
`}</style>
|
|
</>
|
|
)
|
|
}
|
|
|
|
/* ─── ESSAY — literary calm ─────────────────────────────────────────────── */
|
|
function EssayPage({ note, bodyHtml, readingTime, slug, isStale }: PageProps) {
|
|
const dateLabel = formatDate(note.publishedAt)
|
|
return (
|
|
<>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
|
|
<link href="https://fonts.googleapis.com/css2?family=Literata:ital,opsz,wght@0,7..72,400;0,7..72,600;0,7..72,700;1,7..72,400;1,7..72,600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
<ReadingProgress accent="#7A5C2E" />
|
|
|
|
<div className="es-root pub-root" data-pub-root>
|
|
<nav className="es-nav pub-nav">
|
|
<span className="es-brand">Memento</span>
|
|
<div className="es-nav-right">
|
|
<CopyLinkButton className="es-copy pub-copy" label="Copier le lien" copiedLabel="Copié" />
|
|
<span className="es-badge"><Sparkles size={10} /> Essai</span>
|
|
<a href={`/p/${slug}/report`} className="es-report pub-report" aria-label="Signaler"><Flag size={12} /></a>
|
|
</div>
|
|
</nav>
|
|
|
|
<main className="es-main">
|
|
<div className="es-ornament" aria-hidden>
|
|
<span className="es-ornament-line" />
|
|
<span className="es-ornament-diamond" />
|
|
<span className="es-ornament-line" />
|
|
</div>
|
|
|
|
<header className="es-header">
|
|
{dateLabel && <time className="es-date">{dateLabel}</time>}
|
|
<h1 className="es-title">{note.title || 'Sans titre'}</h1>
|
|
<div className="es-meta-row">
|
|
{note.user?.name && (
|
|
<div className="es-author">
|
|
{note.user.image && <img src={note.user.image} alt="" className="es-avatar" />}
|
|
<span className="es-author-name">{note.user.name}</span>
|
|
</div>
|
|
)}
|
|
<span className="es-reading-time">{readingTime} min de lecture</span>
|
|
</div>
|
|
</header>
|
|
|
|
<div dir="auto" className="es-body pub-article" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
|
|
|
|
<footer className="es-footer">
|
|
<div className="es-ornament" aria-hidden>
|
|
<span className="es-ornament-line" />
|
|
<span className="es-ornament-diamond" />
|
|
<span className="es-ornament-line" />
|
|
</div>
|
|
<p>Publié sur <strong>Memento</strong></p>
|
|
{isStale && <p className="es-stale">Le contenu source a évolué depuis la publication.</p>}
|
|
</footer>
|
|
</main>
|
|
</div>
|
|
|
|
<style>{`
|
|
${BASE_RESET}
|
|
${ARTICLE_TYPO}
|
|
.es-root {
|
|
--pub-accent: #7A5C2E;
|
|
min-height: 100vh;
|
|
background:
|
|
radial-gradient(ellipse 100% 60% at 50% 0%, #f7f1e6 0%, transparent 55%),
|
|
#fbf8f2;
|
|
color: #2c2416;
|
|
font-family: 'Inter', system-ui, sans-serif;
|
|
}
|
|
|
|
.es-nav {
|
|
position: sticky; top: 0; z-index: 30;
|
|
background: rgba(251,248,242,0.88);
|
|
backdrop-filter: blur(14px);
|
|
border-bottom: 1px solid rgba(92,74,42,0.1);
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 0 28px; height: 52px;
|
|
}
|
|
.es-brand {
|
|
font-family: 'Literata', Georgia, serif;
|
|
font-size: 16px; font-style: italic; color: #5c4a2a; font-weight: 600;
|
|
}
|
|
.es-nav-right { display: flex; align-items: center; gap: 12px; }
|
|
.es-badge {
|
|
display: none; align-items: center; gap: 4px;
|
|
font-size: 11px; font-weight: 500; color: #8b6c3a;
|
|
}
|
|
@media (min-width: 640px) { .es-badge { display: flex; } }
|
|
.es-copy {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
font-size: 11px; font-weight: 500; color: #6b5337;
|
|
background: transparent; border: 1px solid #d8cebc;
|
|
border-radius: 999px; padding: 5px 12px; cursor: pointer;
|
|
font-family: inherit; transition: background .15s;
|
|
}
|
|
.es-copy:hover { background: rgba(122,92,46,0.06); }
|
|
.es-report { color: #b5a48a; display: flex; transition: color .15s; }
|
|
.es-report:hover { color: #5c4a2a; }
|
|
|
|
.es-main { max-width: 680px; margin: 0 auto; padding: 40px 28px 100px; }
|
|
|
|
.es-ornament {
|
|
display: flex; align-items: center; justify-content: center; gap: 12px;
|
|
margin: 28px 0 8px;
|
|
}
|
|
.es-ornament-line {
|
|
width: 48px; height: 1px;
|
|
background: linear-gradient(90deg, transparent, #c8b99a, transparent);
|
|
}
|
|
.es-ornament-diamond {
|
|
width: 7px; height: 7px; background: #7A5C2E;
|
|
transform: rotate(45deg); opacity: 0.7;
|
|
}
|
|
|
|
.es-header { text-align: center; padding: 28px 0 8px; }
|
|
.es-date {
|
|
display: block; font-size: 11px; font-weight: 600;
|
|
letter-spacing: 0.2em; text-transform: uppercase;
|
|
color: #a08b5c; margin-bottom: 22px;
|
|
}
|
|
.es-title {
|
|
font-family: 'Literata', Georgia, serif;
|
|
font-size: clamp(1.85rem, 5.2vw, 3.1rem);
|
|
font-weight: 600; font-style: italic; line-height: 1.18;
|
|
letter-spacing: -0.02em; color: #1a140c;
|
|
margin-bottom: 28px; text-wrap: balance;
|
|
}
|
|
.es-meta-row {
|
|
display: flex; flex-wrap: wrap; align-items: center;
|
|
justify-content: center; gap: 16px;
|
|
padding-top: 22px; border-top: 1px solid #e2d9ca;
|
|
}
|
|
.es-author { display: flex; align-items: center; gap: 10px; }
|
|
.es-avatar {
|
|
width: 36px; height: 36px; border-radius: 50%;
|
|
border: 1.5px solid #d8cebc; object-fit: cover;
|
|
}
|
|
.es-author-name { font-size: 13px; font-weight: 600; color: #3d2f14; }
|
|
.es-reading-time { font-size: 12px; color: #a08b5c; }
|
|
|
|
.es-body {
|
|
margin-top: 48px;
|
|
font-family: 'Literata', Georgia, serif;
|
|
font-size: 19.5px; line-height: 1.9; color: #2c2416;
|
|
font-optical-sizing: auto;
|
|
}
|
|
.es-body .pub-tpl { all: unset; display: block; }
|
|
|
|
.es-body .pub-epigraph {
|
|
position: relative; text-align: center;
|
|
font-size: 1.12em; font-style: italic; color: #6b5337;
|
|
margin: 0 0 2.75em; padding: 1.6em 1.5em 1.2em;
|
|
border-top: 1px solid #d8cebc; border-bottom: 1px solid #d8cebc;
|
|
}
|
|
.es-body .pub-epigraph::before {
|
|
content: '“';
|
|
position: absolute; top: -0.25em; left: 50%; transform: translateX(-50%);
|
|
font-size: 3.5em; color: #d8cebc; line-height: 1;
|
|
font-family: 'Literata', Georgia, serif; pointer-events: none;
|
|
}
|
|
|
|
.es-body .pub-essay-summary {
|
|
font-size: 1.12em; line-height: 1.75; color: #4a3726;
|
|
margin-bottom: 2.25em; font-style: italic;
|
|
padding-left: 1.25em; border-left: 2px solid #c8b99a;
|
|
}
|
|
|
|
.es-body .pub-body-source > p:first-of-type::first-letter {
|
|
font-family: 'Literata', Georgia, serif;
|
|
float: left; font-size: 3.6em; line-height: 0.82;
|
|
padding-right: 0.08em; padding-top: 0.06em;
|
|
font-weight: 700; color: #1a140c;
|
|
}
|
|
.es-body .pub-body-source p { text-indent: 1.5em; margin: 0 0 0.15em; }
|
|
.es-body .pub-body-source p:first-of-type { text-indent: 0; }
|
|
.es-body .pub-rewrite-body p { margin: 0.9em 0; }
|
|
|
|
.es-body .pub-hero {
|
|
margin: 0 -12px 2.75em; overflow: hidden;
|
|
max-height: 440px; aspect-ratio: 16/9; background: #e8e0d0;
|
|
border-radius: 2px;
|
|
}
|
|
@media (min-width: 720px) {
|
|
.es-body .pub-hero { margin-left: -40px; margin-right: -40px; }
|
|
}
|
|
.es-body .pub-hero-img {
|
|
width: 100%; height: 100%; object-fit: cover; display: block;
|
|
filter: sepia(8%) contrast(1.02);
|
|
}
|
|
|
|
.es-body h2 {
|
|
font-family: 'Literata', serif; font-size: 1.4em; font-style: italic;
|
|
font-weight: 600; margin-top: 2.4em; margin-bottom: 0.65em; color: #1a140c;
|
|
}
|
|
.es-body h3 {
|
|
font-family: 'Literata', serif; font-size: 1.15em;
|
|
font-weight: 600; margin-top: 1.9em; margin-bottom: 0.45em;
|
|
}
|
|
.es-body p { text-wrap: pretty; }
|
|
.es-body blockquote {
|
|
border: none; margin: 2.25em 0; padding: 0 1.5em;
|
|
font-size: 1.12em; color: #5c4a2a; font-style: italic;
|
|
text-align: center;
|
|
}
|
|
.es-body ul, .es-body ol { padding-left: 1.35em; margin: 1em 0; }
|
|
.es-body li { margin: 0.45em 0; }
|
|
.es-body pre {
|
|
background: #f2ede4; padding: 20px; border-radius: 6px;
|
|
overflow-x: auto; font-family: 'SF Mono', Menlo, monospace;
|
|
font-size: 13px; margin: 1.5em 0;
|
|
}
|
|
.es-body code {
|
|
font-family: 'SF Mono', Menlo, monospace; font-size: 0.85em;
|
|
background: #ede7da; padding: 1px 5px; border-radius: 3px;
|
|
}
|
|
.es-body pre code { background: none; padding: 0; }
|
|
.es-body table { border-collapse: collapse; width: 100%; margin: 1.5em 0; }
|
|
.es-body th {
|
|
border-bottom: 2px solid #5c4a2a; padding: 8px 12px;
|
|
text-align: left; font-size: 13px; color: #5c4a2a;
|
|
}
|
|
.es-body td { border-bottom: 1px solid #e8e0d0; padding: 8px 12px; }
|
|
.es-body a {
|
|
color: #7a5c2e; text-decoration: underline;
|
|
text-decoration-style: dotted; text-underline-offset: 3px;
|
|
}
|
|
.es-body .pub-figure-img { filter: sepia(5%); border-radius: 2px; }
|
|
.es-body .pub-callout { border-radius: 4px; padding: 12px 16px; margin: 1.5em 0; }
|
|
${KATEX_PUBLISH_CSS}
|
|
|
|
.es-footer {
|
|
text-align: center; padding: 48px 0 0;
|
|
font-size: 13px; color: #a08b5c;
|
|
}
|
|
.es-footer strong { color: #5c4a2a; }
|
|
.es-stale { font-size: 11px; color: #c4b49a; margin-top: 8px; }
|
|
|
|
.es-body {
|
|
--pub-accent: #7a5c2e;
|
|
--pub-summary-color: #5c4a2a;
|
|
--pub-exercise-border: #d8cebc;
|
|
--pub-exercise-header-bg: #f5f0e8;
|
|
--pub-solution-bg: rgba(122,92,46,0.05);
|
|
--pub-definition-border: #d8cebc;
|
|
--pub-definition-bg: #fdfbf7;
|
|
--pub-toggle-border: #e2d9ca;
|
|
--pub-toggle-header-bg: #f7f3ec;
|
|
--pub-highlight-bg: #fdf6eb;
|
|
--pub-checklist-bg: rgba(0,0,0,0.025);
|
|
}
|
|
.es-body .pub-rewrite-body .pub-code { background: #2c2416; }
|
|
.es-body .pub-rewrite-body .pub-code code { background: #2c2416; color: #f0e8da; }
|
|
${REWRITE_SHARED_CSS}
|
|
`}</style>
|
|
</>
|
|
)
|
|
}
|
|
|
|
/* ─── SIMPLE — clean Substack-grade default ─────────────────────────────── */
|
|
function SimplePage({ note, bodyHtml, readingTime, slug, isStale }: PageProps) {
|
|
const dateLabel = formatDate(note.publishedAt, 'd MMM yyyy')
|
|
return (
|
|
<>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
|
|
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;0,8..60,700;1,8..60,400&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
<ReadingProgress accent="#A47148" />
|
|
|
|
<div className="sp-root pub-root" data-pub-root>
|
|
<nav className="sp-nav pub-nav">
|
|
<span className="sp-brand">Memento</span>
|
|
<div className="sp-nav-right">
|
|
<CopyLinkButton className="sp-copy pub-copy" label="Copier le lien" copiedLabel="Copié" />
|
|
<a href={`/p/${slug}/report`} className="sp-report pub-report">
|
|
<Flag size={12} /> Signaler
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<article className="sp-article">
|
|
<header className="sp-header">
|
|
<div className="sp-kicker">
|
|
{dateLabel && <span>{dateLabel}</span>}
|
|
{dateLabel && <span className="sp-dot" />}
|
|
<span>{readingTime} min de lecture</span>
|
|
</div>
|
|
<h1 className="sp-title">{note.title || 'Sans titre'}</h1>
|
|
{note.user?.name && (
|
|
<div className="sp-author">
|
|
{note.user.image && <img src={note.user.image} alt="" className="sp-avatar" />}
|
|
<span>par <strong>{note.user.name}</strong></span>
|
|
</div>
|
|
)}
|
|
</header>
|
|
|
|
<div className="sp-rule" />
|
|
|
|
<div dir="auto" className="sp-body pub-article" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
|
|
</article>
|
|
|
|
<footer className="sp-footer">
|
|
<span className="sp-footer-brand">Memento</span>
|
|
<span> — Votre mémoire augmentée par l’IA</span>
|
|
{isStale && (
|
|
<p className="sp-stale">Le contenu source a évolué depuis la publication.</p>
|
|
)}
|
|
</footer>
|
|
</div>
|
|
|
|
<style>{`
|
|
${BASE_RESET}
|
|
${ARTICLE_TYPO}
|
|
.sp-root {
|
|
--pub-accent: #A47148;
|
|
min-height: 100vh;
|
|
background: #faf9f5;
|
|
color: #1a1a1a;
|
|
font-family: 'Inter', system-ui, sans-serif;
|
|
}
|
|
|
|
.sp-nav {
|
|
position: sticky; top: 0; z-index: 30;
|
|
background: rgba(250,249,245,0.9);
|
|
backdrop-filter: blur(14px);
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 0 24px; height: 52px;
|
|
}
|
|
.sp-brand {
|
|
font-family: 'Source Serif 4', Georgia, serif;
|
|
font-weight: 700; font-size: 16px; color: #1a1a1a;
|
|
}
|
|
.sp-nav-right { display: flex; align-items: center; gap: 10px; }
|
|
.sp-copy {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
font-size: 11px; font-weight: 500; color: #555;
|
|
background: #fff; border: 1px solid rgba(0,0,0,0.08);
|
|
border-radius: 999px; padding: 5px 12px; cursor: pointer;
|
|
font-family: inherit; box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
|
transition: border-color .15s, color .15s;
|
|
}
|
|
.sp-copy:hover { border-color: #A47148; color: #A47148; }
|
|
.sp-report {
|
|
display: flex; align-items: center; gap: 5px;
|
|
font-size: 12px; color: #888; text-decoration: none;
|
|
padding: 5px 10px; border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.08); transition: color .15s, border-color .15s;
|
|
}
|
|
.sp-report:hover { color: #333; border-color: rgba(0,0,0,0.15); }
|
|
|
|
.sp-article {
|
|
max-width: 680px; margin: 0 auto;
|
|
padding: 48px 24px 72px;
|
|
}
|
|
.sp-header { margin-bottom: 8px; }
|
|
.sp-kicker {
|
|
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
|
|
font-size: 12px; font-weight: 500; color: #888; margin-bottom: 16px;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
.sp-dot { width: 3px; height: 3px; border-radius: 50%; background: #ccc; }
|
|
.sp-title {
|
|
font-family: 'Source Serif 4', Georgia, serif;
|
|
font-size: clamp(1.75rem, 4.5vw, 2.5rem);
|
|
font-weight: 700; line-height: 1.18; letter-spacing: -0.025em;
|
|
margin: 0 0 20px; text-wrap: balance; color: #111;
|
|
}
|
|
.sp-author {
|
|
display: flex; align-items: center; gap: 10px;
|
|
font-size: 14px; color: #666;
|
|
}
|
|
.sp-author strong { color: #333; font-weight: 600; }
|
|
.sp-avatar {
|
|
width: 32px; height: 32px; border-radius: 50%;
|
|
object-fit: cover; border: 1px solid rgba(0,0,0,0.08);
|
|
}
|
|
.sp-rule {
|
|
height: 1px; background: linear-gradient(90deg, transparent, rgba(0,0,0,0.1), transparent);
|
|
margin: 32px 0 36px;
|
|
}
|
|
|
|
.sp-body {
|
|
font-family: 'Source Serif 4', Georgia, serif;
|
|
font-size: 18px; line-height: 1.85; color: #1f1f1f;
|
|
font-optical-sizing: auto;
|
|
}
|
|
.sp-body h2 {
|
|
font-size: 1.4em; font-weight: 700; margin-top: 2em; margin-bottom: 0.55em;
|
|
letter-spacing: -0.02em; color: #111;
|
|
}
|
|
.sp-body h3 { font-size: 1.15em; font-weight: 600; margin-top: 1.7em; margin-bottom: 0.45em; }
|
|
.sp-body p { margin: 1em 0; text-wrap: pretty; }
|
|
.sp-body .pub-body-source > p:first-of-type::first-letter {
|
|
font-weight: 700; color: #A47148;
|
|
}
|
|
.sp-body ul, .sp-body ol { padding-left: 1.4em; margin: 1em 0; }
|
|
.sp-body li { margin: 0.4em 0; }
|
|
.sp-body blockquote {
|
|
border-left: 3px solid #A47148; padding: 2px 0 2px 1.1em;
|
|
margin: 1.4em 0; color: #555; font-style: italic;
|
|
}
|
|
.sp-body pre {
|
|
background: #f0ede6; padding: 18px; border-radius: 10px;
|
|
overflow-x: auto; font-size: 13.5px; margin: 1.4em 0;
|
|
}
|
|
.sp-body code {
|
|
font-family: 'SF Mono', Menlo, monospace; font-size: 0.87em;
|
|
background: #f0ede6; padding: 1px 5px; border-radius: 4px;
|
|
}
|
|
.sp-body pre code { background: none; padding: 0; }
|
|
.sp-body table { border-collapse: collapse; width: 100%; margin: 1.4em 0; }
|
|
.sp-body th, .sp-body td { border: 1px solid #e0dcd3; padding: 9px 12px; text-align: left; }
|
|
.sp-body th { background: #f4f1ea; font-weight: 600; font-size: 13px; }
|
|
.sp-body a {
|
|
color: #A47148; text-decoration: underline; text-underline-offset: 3px;
|
|
}
|
|
.sp-body .pub-callout { border-radius: 10px; padding: 12px 16px; margin: 1.1em 0; }
|
|
.sp-body .pub-hero {
|
|
margin: 0 0 2rem; border-radius: 12px; overflow: hidden;
|
|
max-height: 420px; aspect-ratio: 16/9; background: #e8e4dc;
|
|
}
|
|
.sp-body .pub-hero-img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
${KATEX_PUBLISH_CSS}
|
|
${REWRITE_SHARED_CSS}
|
|
|
|
.sp-footer {
|
|
border-top: 1px solid rgba(0,0,0,0.07);
|
|
padding: 36px 24px 48px; text-align: center;
|
|
font-size: 13px; color: #999;
|
|
}
|
|
.sp-footer-brand {
|
|
font-family: 'Source Serif 4', serif;
|
|
font-weight: 700; color: #A47148;
|
|
}
|
|
.sp-stale { margin-top: 10px; font-size: 11px; color: #bbb; }
|
|
|
|
.sp-body {
|
|
--pub-accent: #A47148;
|
|
--pub-summary-color: #555;
|
|
--pub-exercise-border: #e0d4c3;
|
|
--pub-exercise-header-bg: #f6f1ea;
|
|
--pub-solution-bg: rgba(164,113,72,0.05);
|
|
--pub-definition-border: #e0d4c3;
|
|
--pub-definition-bg: #faf8f4;
|
|
--pub-toggle-border: #e8e4dc;
|
|
--pub-toggle-header-bg: #f5f2eb;
|
|
--pub-highlight-bg: #faf6ee;
|
|
--pub-checklist-bg: rgba(0,0,0,0.03);
|
|
}
|
|
`}</style>
|
|
</>
|
|
)
|
|
}
|
|
|
|
/* ─── Types ─────────────────────────────────────────────────────────────── */
|
|
type NoteData = NonNullable<Awaited<ReturnType<typeof getPublishedNote>>>
|
|
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 <SimplePage {...props} />
|
|
if (note.publishedTemplate === 'brief') return <BriefPage {...props} />
|
|
if (note.publishedTemplate === 'essay') return <EssayPage {...props} />
|
|
return <MagazinePage {...props} />
|
|
}
|