6b509fe052
- 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>
252 lines
11 KiB
Plaintext
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>
|