fix: page publique — plus de <html>/<body> (utilise le layout existant)
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 4s
CI / Deploy production (on server) (push) Has been skipped

- Retire <html>/<body>/<head> qui créaient des erreurs d'hydration
- Utilise <link> + <style> dans le fragment React (compatible Next.js)
- Garde KaTeX CSS + Google Fonts + styles inline
- Plus d'erreurs de nesting HTML
This commit is contained in:
Antigravity
2026-06-19 22:15:53 +00:00
parent a6cdcba76f
commit e02a9d9a53

View File

@@ -1,6 +1,6 @@
import { notFound } from 'next/navigation'
import { getPublishedNote } from '@/app/actions/notes-publishing'
import { FileText, Calendar, Clock, Flag } from 'lucide-react'
import { Calendar, Clock, Flag } from 'lucide-react'
import { format } from 'date-fns'
import katex from 'katex'
@@ -16,97 +16,81 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
export const revalidate = 60
function renderMathInHtml(html: string): string {
let result = html
result = result.replace(/<div[^>]*data-type="math-equation"[^>]*data-latex="([^"]*)"[^>]*><\/div>/g, (_, latex) => {
try {
return `<div class="math-display">${katex.renderToString(decodeHtml(latex), { displayMode: true, throwOnError: false })}</div>`
} catch { return `<div class="math-display">${latex}</div>` }
})
result = result.replace(/<span[^>]*data-type="inline-math"[^>]*data-latex="([^"]*)"[^>]*>.*?<\/span>/g, (_, latex) => {
try {
return katex.renderToString(decodeHtml(latex), { displayMode: false, throwOnError: false })
} catch { return latex }
})
return result
}
function decodeHtml(text: string): string {
return text.replace(/&quot;/g, '"').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&#39;/g, "'")
const map: Record<string, string> = { '&quot;': '"', '&amp;': '&', '&lt;': '<', '&gt;': '>', '&#39;': "'" }
return text.replace(/&[a-z#0-9]+;/gi, m => map[m] || m)
}
function processContent(html: string): string {
let result = renderMathInHtml(html)
// Render callouts
let result = html
// KaTeX block math
result = result.replace(/<div[^>]*data-type="math-equation"[^>]*data-latex="([^"]*)"[^>]*><\/div>/g, (_, latex) => {
try { return `<div class="r-math-display">${katex.renderToString(decodeHtml(latex), { displayMode: true, throwOnError: false })}</div>` }
catch { return `<div class="r-math-display">${latex}</div>` }
})
// KaTeX inline math
result = result.replace(/<span[^>]*data-type="inline-math"[^>]*data-latex="([^"]*)"[^>]*>.*?<\/span>/g, (_, latex) => {
try { return katex.renderToString(decodeHtml(latex), { displayMode: false, throwOnError: false }) }
catch { return latex }
})
// Callouts
result = result.replace(/<div[^>]*data-type="callout-block"[^>]*data-callout-type="([^"]*)"[^>]*>/g, (_, type) => {
const colors: Record<string, string> = {
const c: Record<string, string> = {
info: '#eff6ff|#3b82f6', warning: '#fffbeb|#f59e0b', tip: '#faf5ff|#8b5cf6',
success: '#f0fdf4|#22c55e', danger: '#fef2f2|#ef4444',
}
const [bg, border] = (colors[type] || colors.info).split('|')
const [bg, border] = (c[type] || c.info).split('|')
return `<div style="background:${bg};border-left:4px solid ${border};border-radius:8px;padding:12px 16px;margin:16px 0">`
})
// Close callouts
result = result.replace(/<\/div>(\s*<\/div>)/g, '$1')
// Outline blocks — remove (they don't work without the editor)
// Remove outline blocks (need editor JS)
result = result.replace(/<div[^>]*data-type="outline-block"[^>]*><\/div>/g, '')
// Link preview searchable text
// Remove link preview searchable text
result = result.replace(/<div[^>]*class="link-preview-searchable"[^>]*>[\s\S]*?<\/div>/g, '')
// Remove buttons (delete/unwrap)
// Remove editor buttons
result = result.replace(/<button[^>]*>[\s\S]*?<\/button>/g, '')
// Remove contentEditable attributes
result = result.replace(/contenteditable="[^"]*"/gi, '')
return result
}
function estimateReadingTime(html: string): number {
const text = html.replace(/<[^>]+>/g, ' ').trim()
const words = text.split(/\s+/).length
const words = html.replace(/<[^>]+>/g, ' ').trim().split(/\s+/).length
return Math.max(1, Math.ceil(words / 200))
}
export default async function PublishedNotePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const note = await getPublishedNote(slug)
if (!note) notFound()
const processedContent = processContent(note.content || '')
const readingTime = estimateReadingTime(note.content || '')
return (
<html lang="fr">
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,300..600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
</head>
<body style={{
margin: 0, background: '#FAF9F5', color: '#1a1a1a',
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
lineHeight: 1.7,
}}>
<>
{/* KaTeX + fonts CSS */}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,300..600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
<div style={{ minHeight: '100vh', background: '#FAF9F5', color: '#1a1a1a', fontFamily: "'Inter', sans-serif" }}>
{/* Top bar */}
<div style={{
position: 'sticky', top: 0, zIndex: 10,
background: 'rgba(250, 249, 245, 0.85)', backdropFilter: 'blur(12px)',
background: 'rgba(250,249,245,0.85)', backdropFilter: 'blur(12px)',
borderBottom: '1px solid rgba(0,0,0,0.06)', padding: '10px 24px',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontFamily: "'Source Serif 4', serif", fontWeight: 600, fontSize: '15px' }}>Momento</span>
</div>
<a href={`/p/${slug}/report`} style={{
display: 'flex', alignItems: 'center', gap: '4px',
fontSize: '12px', color: '#888', textDecoration: 'none',
padding: '4px 10px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.1)',
transition: 'all 0.15s',
}}>
<span style={{ fontFamily: "'Source Serif 4', serif", fontWeight: 600, fontSize: '15px' }}>Momento</span>
<a href={`/p/${slug}/report`} style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px', color: '#888', textDecoration: 'none', padding: '4px 10px', borderRadius: '6px', border: '1px solid rgba(0,0,0,0.1)' }}>
<Flag size={12} /> Signaler
</a>
</div>
{/* Article */}
<article style={{ maxWidth: '720px', margin: '0 auto', padding: '48px 24px 80px' }}>
{/* Meta */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', color: '#999', marginBottom: '16px' }}>
<Calendar size={12} />
<span>{note.publishedAt ? format(new Date(note.publishedAt), 'd MMM yyyy') : ''}</span>
@@ -115,58 +99,30 @@ export default async function PublishedNotePage({ params }: { params: Promise<{
<span>{readingTime} min de lecture</span>
</div>
{/* Title */}
<h1 style={{
fontFamily: "'Source Serif 4', Georgia, serif",
fontSize: 'clamp(28px, 5vw, 40px)', fontWeight: 600,
lineHeight: 1.2, letterSpacing: '-0.02em', margin: '0 0 12px',
}}>
<h1 style={{ fontFamily: "'Source Serif 4', Georgia, serif", fontSize: 'clamp(28px,5vw,40px)', fontWeight: 600, lineHeight: 1.2, letterSpacing: '-0.02em', margin: '0 0 12px' }}>
{note.title || 'Sans titre'}
</h1>
{/* Author */}
{note.user?.name && (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '40px' }}>
{note.user.image && (
<img src={note.user.image} alt="" style={{ width: '28px', height: '28px', borderRadius: '50%' }} />
)}
{note.user.image && <img src={note.user.image} alt="" style={{ width: '28px', height: '28px', borderRadius: '50%' }} />}
<span style={{ fontSize: '14px', color: '#666' }}>par {note.user.name}</span>
</div>
)}
{/* Divider */}
<div style={{ height: '1px', background: 'rgba(0,0,0,0.08)', marginBottom: '40px' }} />
{/* Content */}
<div
dir="auto"
style={{ fontSize: '16px', lineHeight: 1.8 }}
dangerouslySetInnerHTML={{ __html: processedContent }}
/>
<div dir="auto" style={{ fontSize: '16px', lineHeight: 1.8 }} dangerouslySetInnerHTML={{ __html: processedContent }} />
{/* Custom styles injected */}
<style>{`
article h1, article h2, article h3 {
font-family: 'Source Serif 4', Georgia, serif;
font-weight: 600;
letter-spacing: -0.01em;
margin-top: 1.8em;
margin-bottom: 0.6em;
}
article h1, article h2, article h3 { font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; letter-spacing: -0.01em; margin-top: 1.8em; margin-bottom: 0.6em; }
article h2 { font-size: 1.5em; }
article h3 { font-size: 1.2em; }
article p { margin: 1em 0; }
article ul, article ol { padding-left: 1.5em; margin: 1em 0; }
article li { margin: 0.4em 0; }
article blockquote {
border-left: 3px solid #A47148;
padding-left: 1em; margin: 1.2em 0;
color: #555; font-style: italic;
}
article pre {
background: #f4f3ef; padding: 16px; border-radius: 8px;
overflow-x: auto; font-size: 14px; margin: 1.2em 0;
}
article blockquote { border-left: 3px solid #A47148; padding-left: 1em; margin: 1.2em 0; color: #555; font-style: italic; }
article pre { background: #f4f3ef; padding: 16px; border-radius: 8px; overflow-x: auto; font-size: 14px; margin: 1.2em 0; }
article code { font-family: 'SF Mono', Menlo, monospace; }
article table { border-collapse: collapse; width: 100%; margin: 1.2em 0; }
article th, article td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
@@ -174,23 +130,16 @@ export default async function PublishedNotePage({ params }: { params: Promise<{
article img { max-width: 100%; height: auto; border-radius: 8px; margin: 1em 0; }
article a { color: #A47148; text-decoration: none; }
article a:hover { text-decoration: underline; }
.math-display { margin: 1.5em 0; text-align: center; overflow-x: auto; }
.toggle-block { border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; margin: 1em 0; overflow: hidden; }
.toggle-block > div:first-child { background: #f4f3ef; padding: 8px 12px; font-size: 13px; font-weight: 600; }
[data-type="columns"] { display: flex; gap: 16px; margin: 1em 0; }
[data-type="columns"] > div { flex: 1; }
.r-math-display { margin: 1.5em 0; text-align: center; overflow-x: auto; }
`}</style>
</article>
{/* Footer */}
<footer style={{
borderTop: '1px solid rgba(0,0,0,0.08)', padding: '32px 24px',
textAlign: 'center', fontSize: '13px', color: '#999',
}}>
<footer style={{ borderTop: '1px solid rgba(0,0,0,0.08)', padding: '32px 24px', textAlign: 'center', fontSize: '13px', color: '#999' }}>
<span style={{ fontFamily: "'Source Serif 4', serif", fontWeight: 600, color: '#A47148' }}>Momento</span>
{' — Votre mémoire augmentée par l\'IA'}
</footer>
</body>
</html>
</div>
</>
)
}