import Link from 'next/link'; import Image from 'next/image'; import type { Metadata } from 'next'; import { cookies } from 'next/headers'; import { type JournalEntrySummary } from '@/lib/journal'; import { formatJournalDate, journalDateTime } from '@/lib/journalDate'; import { listJournalEntriesForRequest } from '@/lib/journal.server'; import { isAdminAuthenticated } from '@/lib/admin/auth'; import { isAuthenticated } from '@/lib/auth'; import { immich } from '@/lib/immich'; import { imageUrl, assetPlaceholder } from '@/lib/urls'; import { ogImageUrl } from '@/lib/ogImage'; import { BackLink } from '@/components/BackLink'; import { IconBook } from '@/components/Icons'; import { getServerDictionary } from '@/lib/i18n/server'; import './journal.css'; export const dynamic = 'force-dynamic'; /** * Without an openGraph of its own the index inherited the layout's, so a * shared /journal link previewed as the site's home page. The description is * the subtitle the page shows, not a second sentence found nowhere on it. */ export function generateMetadata(): Metadata { const t = getServerDictionary(); const title = t.journal.title; const description = t.journal.subtitle; const ogImage = ogImageUrl(title); return { title, description, openGraph: { title, description, images: [ogImage] }, twitter: { card: 'summary_large_image', title, description, images: [ogImage] }, }; } interface EnrichedJournalEntry extends JournalEntrySummary { coverUrl?: string; blurDataURL?: string; dominantColor?: string; } export default async function JournalIndexPage() { const t = getServerDictionary(); const isAuthedAdmin = await isAdminAuthenticated(); const allEntries = await listJournalEntriesForRequest(); // Non-admins only see published entries, and only a password-protected one // once they have actually unlocked it — the card otherwise names it, shows // its cover and quotes its excerpt to every visitor regardless of the // password on its body (GHSA-fvgv-97g3-wjr7). const cookieStore = isAuthedAdmin ? null : await cookies(); const getCookie = (name: string) => cookieStore?.get(name)?.value; const visibleEntries = isAuthedAdmin ? allEntries : allEntries.filter((e) => { if (e.frontmatter.draft) return false; if (!e.frontmatter.password) return true; return isAuthenticated(e.slug, getCookie, 'journal'); }); // Fetch cover image placeholders and URLs const enrichedEntries: EnrichedJournalEntry[] = await Promise.all( visibleEntries.map(async (entry) => { if (!entry.frontmatter.coverAssetId) { return entry; } try { const asset = await immich.getAssetInfo(entry.frontmatter.coverAssetId); const ph = asset ? assetPlaceholder(asset) : null; return { ...entry, coverUrl: imageUrl(asset ?? entry.frontmatter.coverAssetId, 'preview'), blurDataURL: ph?.blurDataURL, dominantColor: ph?.dominantColor, }; } catch { return { ...entry, coverUrl: imageUrl(entry.frontmatter.coverAssetId, 'preview'), }; } }), ); return (

{t.journal.title}

{t.journal.subtitle}

{enrichedEntries.length === 0 ? (

{t.journal.empty}

) : (
{enrichedEntries.map((entry) => { const dateStr = formatJournalDate(entry.frontmatter.date, t.dateLocale); const dateTime = journalDateTime(entry.frontmatter.date); return (
{entry.coverUrl ? ( {entry.frontmatter.title ) : (
)}
{dateStr && (dateTime ? ( ) : ( {dateStr} ))} {dateStr && entry.readingTimeMinutes && •} {entry.readingTimeMinutes && ( {t.journal.minRead(entry.readingTimeMinutes)} )} {entry.frontmatter.draft && ( {t.journal.draft} )}

{entry.frontmatter.title || entry.slug}

{entry.frontmatter.subtitle && (

{entry.frontmatter.subtitle}

)} {/* extractExcerpt() falls back to the subtitle when there is one, so rendering both printed the same sentence twice. */} {entry.excerpt && entry.excerpt !== entry.frontmatter.subtitle && (

{entry.excerpt}

)}
{t.journal.readStory} {entry.frontmatter.author && ( {t.journal.by(entry.frontmatter.author)} )}
); })}
)}
); }