First Release v1.0.0
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
---
|
||||
import '@/styles/global.css';
|
||||
import SEO from '@/components/seo/SEO.astro';
|
||||
import JsonLd from '@/components/seo/JsonLd.astro';
|
||||
import Analytics from '@/components/layout/Analytics.astro';
|
||||
import ConsentBanner from '@/components/ui/overlay/ConsentBanner';
|
||||
import { ClientRouter } from 'astro:transitions';
|
||||
import { createWebsiteSchema, createOrganizationSchema, createPersonSchema, createProfessionalServiceSchema } from '@/lib/schema';
|
||||
import type { WebSite, Organization, Person, WithContext } from 'schema-dts';
|
||||
import siteConfig from '@/config/site.config';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
imageAlt?: string;
|
||||
article?: {
|
||||
publishedTime?: Date;
|
||||
modifiedTime?: Date;
|
||||
authors?: string[];
|
||||
tags?: string[];
|
||||
};
|
||||
noindex?: boolean;
|
||||
nofollow?: boolean;
|
||||
includeOrgSchema?: boolean;
|
||||
includePersonSchema?: boolean;
|
||||
includeProfessionalServiceSchema?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
imageAlt,
|
||||
article,
|
||||
noindex = false,
|
||||
nofollow = false,
|
||||
includeOrgSchema = false,
|
||||
includePersonSchema = false,
|
||||
includeProfessionalServiceSchema = false,
|
||||
} = Astro.props;
|
||||
|
||||
// Build JSON-LD schemas
|
||||
const schemas: Array<WithContext<WebSite> | WithContext<Organization> | WithContext<Person>> = [createWebsiteSchema()];
|
||||
if (includeOrgSchema) {
|
||||
schemas.push(createOrganizationSchema());
|
||||
}
|
||||
if (includePersonSchema) {
|
||||
schemas.push(createPersonSchema());
|
||||
}
|
||||
if (includeProfessionalServiceSchema) {
|
||||
schemas.push(createProfessionalServiceSchema());
|
||||
}
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" class="scroll-smooth dark" data-theme="blue">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href={siteConfig.branding.favicon.svg} />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="theme-color" content={siteConfig.branding.colors.themeColor} />
|
||||
|
||||
<!-- SEO -->
|
||||
<SEO
|
||||
title={title}
|
||||
description={description}
|
||||
image={image}
|
||||
imageAlt={imageAlt}
|
||||
article={article}
|
||||
noindex={noindex}
|
||||
nofollow={nofollow}
|
||||
/>
|
||||
|
||||
<!-- RSS Feed -->
|
||||
<link rel="alternate" type="application/rss+xml" title={`${siteConfig.name} RSS Feed`} href="/rss.xml" />
|
||||
|
||||
<!-- JSON-LD Structured Data -->
|
||||
<JsonLd schema={schemas} />
|
||||
|
||||
<!-- Analytics (loads if PUBLIC_GA_MEASUREMENT_ID or PUBLIC_GTM_ID is set) -->
|
||||
<Analytics />
|
||||
|
||||
<!-- View Transitions (client-side routing with animated page transitions) -->
|
||||
<ClientRouter />
|
||||
|
||||
<!-- Random theme per session — runs synchronously to avoid flash -->
|
||||
<script is:inline>
|
||||
(function () {
|
||||
var THEMES = ['orange','amber','lime','emerald','teal','cyan','sky','blue','indigo','violet','purple','magenta'];
|
||||
var KEY = 'color-theme';
|
||||
var theme;
|
||||
try { theme = sessionStorage.getItem(KEY); } catch (e) {}
|
||||
if (!theme) {
|
||||
theme = THEMES[Math.floor(Math.random() * THEMES.length)];
|
||||
try { sessionStorage.setItem(KEY, theme); } catch (e) {}
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Dynamic favicon: syncs letter (first letter of site name) and color (brand-500) with active theme -->
|
||||
<script define:vars={{ _faviconLetter: siteConfig.name.charAt(0).toUpperCase() }}>
|
||||
(function () {
|
||||
const LETTER = _faviconLetter;
|
||||
|
||||
function updateFavicon() {
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue('--brand-500').trim();
|
||||
if (!color) return;
|
||||
const svg =
|
||||
'<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<rect width="48" height="48" rx="8" fill="' + color + '"/>' +
|
||||
'<text x="24" y="24" text-anchor="middle" dominant-baseline="central" ' +
|
||||
'font-family="Outfit, system-ui, sans-serif" font-weight="700" font-size="34" fill="white">' +
|
||||
LETTER +
|
||||
'</text></svg>';
|
||||
const link = document.querySelector('link[rel="icon"]');
|
||||
if (link) link.setAttribute('href', 'data:image/svg+xml,' + encodeURIComponent(svg));
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', updateFavicon);
|
||||
} else {
|
||||
updateFavicon();
|
||||
}
|
||||
|
||||
if (!window.__faviconListenerInit) {
|
||||
window.__faviconListenerInit = true;
|
||||
|
||||
// Re-run whenever data-theme or dark/light class changes
|
||||
new MutationObserver(function (mutations) {
|
||||
for (let i = 0; i < mutations.length; i++) {
|
||||
if (mutations[i].attributeName === 'data-theme' || mutations[i].attributeName === 'class') {
|
||||
updateFavicon();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).observe(document.documentElement, { attributes: true });
|
||||
|
||||
// Re-run after Astro view transitions swap the head
|
||||
document.addEventListener('astro:after-swap', updateFavicon);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Theme script (runs before body paint; <html> starts with class="dark" as default) -->
|
||||
<script is:inline>
|
||||
(function () {
|
||||
const COLOR_THEMES = ['orange', 'amber', 'lime', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'magenta'];
|
||||
|
||||
function applyTheme(el) {
|
||||
try {
|
||||
// Dark / light mode
|
||||
if (sessionStorage.getItem('theme') === 'light') {
|
||||
el.classList.remove('dark');
|
||||
} else {
|
||||
el.classList.add('dark');
|
||||
}
|
||||
} catch (_) { /* ignored */ }
|
||||
|
||||
try {
|
||||
// Color theme — persisted in sessionStorage so it resets to blue on every new visit
|
||||
const saved = sessionStorage.getItem('color-theme');
|
||||
if (saved && COLOR_THEMES.indexOf(saved) !== -1) {
|
||||
el.setAttribute('data-theme', saved);
|
||||
}
|
||||
} catch (_) { /* ignored */ }
|
||||
}
|
||||
|
||||
applyTheme(document.documentElement);
|
||||
|
||||
if (!window.__themeListenersInit) {
|
||||
window.__themeListenersInit = true;
|
||||
|
||||
document.addEventListener('astro:before-swap', function (e) {
|
||||
applyTheme(e.newDocument.documentElement);
|
||||
});
|
||||
|
||||
document.addEventListener('astro:after-swap', function () {
|
||||
applyTheme(document.documentElement);
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-background text-foreground antialiased">
|
||||
<!-- Skip to content link -->
|
||||
<a
|
||||
href="#main-content"
|
||||
class="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-primary-foreground"
|
||||
>
|
||||
Skip to content
|
||||
</a>
|
||||
|
||||
<slot name="header" />
|
||||
|
||||
<main id="main-content" class="flex-1">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<slot name="footer" />
|
||||
|
||||
<ConsentBanner />
|
||||
|
||||
<!-- Back to top button -->
|
||||
<button
|
||||
id="back-to-top"
|
||||
aria-label="Back to top"
|
||||
class="fixed bottom-6 right-6 z-50 flex h-10 w-10 items-center justify-center rounded-full bg-background border border-border-strong text-foreground-muted shadow-md opacity-0 translate-y-2 pointer-events-none transition-[opacity,transform] duration-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m18 15-6-6-6 6"/></svg>
|
||||
</button>
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
const THRESHOLD = 400;
|
||||
|
||||
function initBackToTop() {
|
||||
const btn = document.getElementById('back-to-top');
|
||||
if (!btn || btn.dataset.bttInit) return;
|
||||
btn.dataset.bttInit = 'true';
|
||||
|
||||
function update() {
|
||||
if (window.scrollY > THRESHOLD) {
|
||||
btn.classList.remove('opacity-0', 'translate-y-2', 'pointer-events-none');
|
||||
btn.classList.add('opacity-100', 'translate-y-0');
|
||||
} else {
|
||||
btn.classList.add('opacity-0', 'translate-y-2', 'pointer-events-none');
|
||||
btn.classList.remove('opacity-100', 'translate-y-0');
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', update, { passive: true });
|
||||
btn.addEventListener('click', function () {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
update();
|
||||
}
|
||||
|
||||
initBackToTop();
|
||||
document.addEventListener('astro:page-load', initBackToTop);
|
||||
document.addEventListener('astro:after-swap', initBackToTop);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Scroll reveal: adds .is-visible to [data-reveal] elements as they enter the viewport -->
|
||||
<script is:inline>
|
||||
(function () {
|
||||
function initReveal() {
|
||||
const els = document.querySelectorAll('[data-reveal]:not(.is-visible)');
|
||||
if (!els.length) return;
|
||||
const observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
|
||||
els.forEach(function (el) { observer.observe(el); });
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initReveal);
|
||||
} else {
|
||||
initReveal();
|
||||
}
|
||||
|
||||
document.addEventListener('astro:after-swap', initReveal);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user