Files
davegilligan-new/src/layouts/BaseLayout.astro
T
daveadmin 6b509fe052 Add complete Open Graph metadata layer across all 20 pages
- Add og:title, og:description, og:image, og:url, og:type, og:site_name to BaseLayout
- Add og:locale (en_GB) + og:locale:alternate (fr_FR, nb_NO) multilingual signals
- Add article:published_time, article:section, article:author on all 5 article pages
- Add Twitter/X summary_large_image card and canonical link on every page
- Generate 13 Jazz Noir branded 1200x630 PNG OG images (satori + resvg-js)
- Add scripts/generate-og-images.mjs + Special Elite font for future regeneration
- Add public/images/articles/ and public/assets/ which were previously untracked

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 01:38:51 +02:00

252 lines
11 KiB
Plaintext

---
import "../styles/global.css";
import CookieBanner from "../components/CookieBanner.astro";
import LocaleCopy from "../components/LocaleCopy.astro";
import LocaleSwitcher from "../components/LocaleSwitcher.astro";
import SectionMark from "../components/SectionMark.astro";
import { getSectionHref, launchSections } from "../data/site";
import { footerCallouts, getChromeCopy, localeMeta, localizedSections, policyLinkCopy } from "../data/locales";
interface Props {
title?: string;
description?: string;
lang?: string;
ogImage?: string;
ogType?: "website" | "article";
ogArticlePublishedTime?: string;
ogArticleSection?: string;
}
const {
title = "Dave Gilligan | Blue Note Logic",
description = "A literary, jazzy, technically serious online magazine for writing, consulting, education, languages, family, and AI.",
lang = "en",
ogImage,
ogType,
ogArticlePublishedTime,
ogArticleSection,
} = Astro.props;
const now = new Date();
const issueDate = {
en: new Intl.DateTimeFormat("en-US", {
month: "long",
day: "numeric",
year: "numeric",
}).format(now),
fr: new Intl.DateTimeFormat("fr-FR", {
day: "numeric",
month: "long",
year: "numeric",
}).format(now),
nb: new Intl.DateTimeFormat("nb-NO", {
day: "numeric",
month: "long",
year: "numeric",
}).format(now),
};
const pathname = Astro.url.pathname.replace(/\/+$/, "") || "/";
const siteUrl = "https://davegilligan.com";
const canonicalUrl = siteUrl + (pathname === "/" ? "" : pathname);
const ogImageAbsolute = siteUrl + (ogImage ?? "/images/og/home.png");
const resolvedOgType = ogType ?? "website";
const activeSlug = pathname === "/" ? "home" : pathname.split("/").filter(Boolean)[0];
const primarySlugs = ["business", "education", "writing", "jazz-music", "ai-lab", "norway"];
const primaryNav = launchSections.filter((section) => primarySlugs.includes(section.slug));
const footerNav = launchSections.filter((section) => !primarySlugs.includes(section.slug));
const articleKey = pathname.startsWith("/articles/norway")
? "norway"
: pathname.startsWith("/articles/kongsberg-jazz-2026")
? "jazz"
: pathname.startsWith("/articles/trivia-and-tunes-april-2026")
? "projects"
: null;
const chromeCopy = getChromeCopy({ activeSlug, issueDate, articleKey });
---
<!doctype html>
<html lang={lang} data-ui-lang="en" data-cookie-analytics="false" data-cookie-embeds="false">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<meta name="generator" content={Astro.generator} />
<meta name="theme-color" content="#f6f0e1" />
<link rel="canonical" href={canonicalUrl} />
<meta property="og:site_name" content="Dave Gilligan" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:type" content={resolvedOgType} />
<meta property="og:image" content={ogImageAbsolute} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content={title} />
<meta property="og:locale" content="en_GB" />
<meta property="og:locale:alternate" content="fr_FR" />
<meta property="og:locale:alternate" content="nb_NO" />
{resolvedOgType === "article" && ogArticlePublishedTime && (
<meta property="article:published_time" content={ogArticlePublishedTime} />
)}
{resolvedOgType === "article" && ogArticleSection && (
<meta property="article:section" content={ogArticleSection} />
)}
<meta property="article:author" content="https://davegilligan.com" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImageAbsolute} />
<link rel="icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Newsreader:opsz,ital,wght@6..72,0,400..700;6..72,1,400..600&family=Special+Elite&family=IBM+Plex+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<title>{title}</title>
<script is:inline>
window.op=window.op||function(){var n=[];return new Proxy(function(){arguments.length&&n.push([].slice.call(arguments))},{get:function(t,r){return"q"===r?n:function(){n.push([r].concat([].slice.call(arguments)))}},has:function(t,r){return"q"===r}})}();
(function(){
var cfg={apiUrl:'https://analytics.bluenotelogic.com/api',clientId:'6a6d90f8-66ea-413e-9b3d-9b7278ac6c09',trackScreenViews:true,trackOutgoingLinks:true,trackAttributes:true};
var opLoaded=false;
function initOP(){if(opLoaded)return;opLoaded=true;window.op('init',cfg);var s=document.createElement('script');s.src='https://analytics.bluenotelogic.com/op1.js';s.defer=true;s.async=true;document.head.appendChild(s);}
if(document.documentElement.getAttribute('data-cookie-analytics')==='true'){initOP();}
window.addEventListener('dg:cookie-consent',function(e){var p=e.detail&&e.detail.preference||'';if(p==='all'||p.indexOf('analytics')!==-1){initOP();}});
})();
</script>
</head>
<body>
<div class="locale-bar" role="navigation" aria-label="Language selection">
<div class="locale-bar__inner">
<button type="button" class="locale-bar__btn" data-lang-button="en" aria-pressed="true">
🇬🇧 <span>{localeMeta.en.switcher}</span>
</button>
<span class="locale-bar__sep" aria-hidden="true">·</span>
<button type="button" class="locale-bar__btn" data-lang-button="fr" aria-pressed="false">
🇫🇷 <span>{localeMeta.fr.switcher}</span>
</button>
<span class="locale-bar__sep" aria-hidden="true">·</span>
<button type="button" class="locale-bar__btn" data-lang-button="nb" aria-pressed="false">
🇳🇴 <span>{localeMeta.nb.switcher}</span>
</button>
</div>
</div>
<header class="site-ribbon">
<div class="container site-ribbon__row">
<LocaleCopy copy={chromeCopy.ribbonIssue} />
<LocaleCopy copy={chromeCopy.ribbonNote} />
</div>
</header>
<div class="site-masthead">
<div class="container masthead">
<div class="masthead__row">
<LocaleCopy copy={chromeCopy.issueLabel} />
<LocaleCopy copy={chromeCopy.mastheadLine} />
</div>
<a href="/" class="masthead__brand masthead__brand--site">
<p class="masthead__domain">davegilligan.com</p>
<strong>Dave Gilligan</strong>
<p class="masthead__prompt">
<LocaleCopy copy={chromeCopy.domainPrompt} />
</p>
<span>
<LocaleCopy copy={chromeCopy.domainLede} />
</span>
</a>
<LocaleSwitcher />
<nav class="site-nav" aria-label="Primary sections">
{primaryNav.map((section) => (
<a
href={getSectionHref(section)}
class={`site-nav__link ${activeSlug === section.slug ? "site-nav__link--active" : ""}`}
>
<SectionMark slug={section.slug} className="section-mark--nav" />
<span>
<LocaleCopy
copy={{
en: localizedSections[section.slug]?.en.title ?? section.title,
fr: localizedSections[section.slug]?.fr.title ?? section.title,
nb: localizedSections[section.slug]?.nb.title ?? section.title,
}}
/>
</span>
</a>
))}
</nav>
</div>
</div>
<slot />
<footer class="site-footer">
<div class="container site-footer__top">
<div class="site-footer__brand">
<p>davegilligan.com</p>
<strong><LocaleCopy copy={chromeCopy.footerHeadline} /></strong>
<span><LocaleCopy copy={chromeCopy.footerBody} /></span>
<span style="font-size:0.78rem;color:var(--ink-whisper);font-family:var(--font-mono);letter-spacing:0.1em;text-transform:uppercase;">Filed under the Subcommission for Multilingual Weather</span>
</div>
<div class="footer-links">
{footerNav.map((section) => (
<a href={getSectionHref(section)}>
<SectionMark slug={section.slug} className="section-mark--footer" />
<span>
<LocaleCopy
copy={{
en: localizedSections[section.slug]?.en.title ?? section.title,
fr: localizedSections[section.slug]?.fr.title ?? section.title,
nb: localizedSections[section.slug]?.nb.title ?? section.title,
}}
/>
</span>
</a>
))}
</div>
</div>
<div class="container site-footer__callouts">
<a class="footer-callout" href={footerCallouts.blueNoteLogic.href} target="_blank" rel="noreferrer">
<p class="footer-callout__label">
<LocaleCopy copy={footerCallouts.blueNoteLogic.label} />
</p>
<strong><LocaleCopy copy={footerCallouts.blueNoteLogic.title} /></strong>
<span><LocaleCopy copy={footerCallouts.blueNoteLogic.body} /></span>
</a>
<a class="footer-callout" href={footerCallouts.gilliganTech.href} target="_blank" rel="noreferrer">
<p class="footer-callout__label">
<LocaleCopy copy={footerCallouts.gilliganTech.label} />
</p>
<strong><LocaleCopy copy={footerCallouts.gilliganTech.title} /></strong>
<span><LocaleCopy copy={footerCallouts.gilliganTech.body} /></span>
</a>
</div>
<div class="container site-footer__policies">
<a class="policy-link" href={policyLinkCopy.privacy.href}>
<strong><LocaleCopy copy={policyLinkCopy.privacy.title} /></strong>
<span><LocaleCopy copy={policyLinkCopy.privacy.body} /></span>
</a>
<a class="policy-link" href={policyLinkCopy.cookies.href}>
<strong><LocaleCopy copy={policyLinkCopy.cookies.title} /></strong>
<span><LocaleCopy copy={policyLinkCopy.cookies.body} /></span>
</a>
</div>
<div class="container footer-note">
<span><LocaleCopy copy={chromeCopy.footerNoteLeft} /></span>
<span><LocaleCopy copy={chromeCopy.footerNoteRight} /></span>
</div>
</footer>
<CookieBanner />
</body>
</html>