Files
narlblog/frontend/src/layouts/Layout.astro
T

181 lines
6.1 KiB
Plaintext

---
import '../styles/global.css';
import '@fontsource-variable/fraunces';
import '@fontsource-variable/eb-garamond';
import '@fontsource-variable/jetbrains-mono';
import ThemeSwitcher from '../components/react/ThemeSwitcher';
import Search from '../components/react/Search';
import LogoutButton from '../components/react/LogoutButton';
import EditableText from '../components/react/EditableText';
interface Props {
title: string;
wide?: boolean;
description?: string;
image?: string;
type?: 'website' | 'article';
}
const { title, wide = false, description, image, type = 'website' } = Astro.props;
const API_URL = process.env.PUBLIC_API_URL || 'http://backend:3000';
const isAdmin = Astro.cookies.get('admin_session')?.value === '1';
let siteConfig: {
title: string;
subtitle: string;
footer: string;
favicon: string;
theme: string;
custom_css: string;
contact_links?: { kind: string; label: string; value: string }[];
} = {
title: "Ela's Atelier",
subtitle: "Works on paper, canvas, and elsewhere",
footer: "Hand-arranged with care",
favicon: "/favicon.svg",
theme: "salon",
custom_css: "",
contact_links: []
};
try {
const res = await fetch(`${API_URL}/api/config`);
if (res.ok) {
siteConfig = await res.json();
}
} catch (e) {
console.error("Failed to fetch config:", e);
}
const fullTitle = `${title} · ${siteConfig.title}`;
const year = new Date().getFullYear();
const hasContact = (siteConfig.contact_links?.length ?? 0) > 0;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" href={siteConfig.favicon} />
<meta name="generator" content={Astro.generator} />
<title>{fullTitle}</title>
{description && <meta name="description" content={description} />}
<meta property="og:title" content={fullTitle} />
{description && <meta property="og:description" content={description} />}
<meta property="og:type" content={type} />
<meta property="og:site_name" content={siteConfig.title} />
<meta property="og:url" content={Astro.url.href} />
{image && <meta property="og:image" content={image} />}
<meta name="twitter:card" content={image ? 'summary_large_image' : 'summary'} />
<meta name="twitter:title" content={fullTitle} />
{description && <meta name="twitter:description" content={description} />}
{image && <meta name="twitter:image" content={image} />}
<link rel="alternate" type="application/rss+xml" title={siteConfig.title} href="/feed.xml" />
{siteConfig.custom_css && <style set:html={siteConfig.custom_css} />}
<script is:inline define:vars={{ defaultTheme: siteConfig.theme }}>
const savedTheme = localStorage.getItem('user-theme') || defaultTheme || 'salon';
document.documentElement.classList.add(savedTheme);
</script>
<script is:inline>
// When a page is restored from the back/forward cache (e.g. after
// saving in the editor and hitting Back), the SSR'd post grid is
// stale. Force a fresh fetch so newly edited tags / titles show up.
window.addEventListener('pageshow', function (e) {
if (e.persisted) window.location.reload();
});
</script>
<slot name="head" />
</head>
<body class="text-text">
<div class="salon-atmosphere" aria-hidden="true"></div>
<header class="border-b border-[var(--surface2)]/60">
<div class="max-w-6xl mx-auto px-6 md:px-10 py-5 md:py-7 flex flex-col md:flex-row md:items-end gap-4 md:gap-6">
<a href="/" class="nameplate group" aria-label="Home">
{isAdmin ? (
<EditableText
client:visible
initial={siteConfig.title}
fieldKey="title"
isAdmin
ariaLabel="site title"
className="nameplate-title"
/>
) : (
<span class="nameplate-title group-hover:text-[var(--mauve)] transition-colors">{siteConfig.title}</span>
)}
{isAdmin ? (
<EditableText
client:visible
initial={siteConfig.subtitle}
fieldKey="subtitle"
isAdmin
ariaLabel="site subtitle"
className="nameplate-subtitle"
/>
) : (
<span class="nameplate-subtitle">{siteConfig.subtitle}</span>
)}
</a>
<div class="flex flex-wrap items-center gap-2 md:gap-3 justify-start md:justify-end md:ml-auto w-full md:w-auto">
{hasContact && (
<a href="/contact" class="topbar-control">Contact</a>
)}
<Search client:idle />
<ThemeSwitcher client:only="react" defaultTheme={siteConfig.theme} />
{isAdmin && (
<div class="flex items-center md:pl-3 md:border-l md:border-[var(--surface2)]/60">
<LogoutButton client:idle />
</div>
)}
</div>
</div>
</header>
<main class={`mx-auto px-6 md:px-10 py-10 md:py-16 ${wide ? 'max-w-[95vw]' : 'max-w-6xl'}`}>
<slot />
</main>
<footer class="max-w-6xl mx-auto px-6 md:px-10 py-12 md:py-16 text-center border-t border-[var(--surface2)]/40 mt-12">
<div class="section-rule mb-6">
<span class="ornament">✦</span>
<span class="font-display italic text-[var(--subtext0)] text-sm">end of catalogue</span>
<span class="ornament">✦</span>
</div>
<p class="font-display italic text-base text-[var(--subtext1)] mb-2">
{isAdmin ? (
<EditableText
client:visible
initial={siteConfig.footer}
fieldKey="footer"
isAdmin
ariaLabel="footer text"
className="inline"
/>
) : siteConfig.footer}
</p>
<div class="text-xs text-[var(--subtext0)] tracking-[0.2em] uppercase mb-3 flex items-center justify-center gap-3 flex-wrap">
<a href="/feed.xml" class="hover:text-[var(--mauve)] transition-colors">RSS Feed</a>
{hasContact && (
<>
<span class="text-[var(--overlay0)]" aria-hidden="true">·</span>
<a href="/contact" class="hover:text-[var(--mauve)] transition-colors">Contact</a>
</>
)}
{!isAdmin && (
<>
<span class="text-[var(--overlay0)]" aria-hidden="true">·</span>
<a href="/admin/login" class="hover:text-[var(--mauve)] transition-colors">Admin login</a>
</>
)}
</div>
<div class="text-[var(--overlay0)] text-xs italic">
&copy; {year} · {siteConfig.title}
</div>
</footer>
</body>
</html>