Initial portfolio site: Astro + Tailwind MVP
Outcome-led hero, about, grouped skills, experience summary, featured projects + /projects index, static contact, SEO/OG, dark/light theme. Dockerfile + nginx config + build script for homelab deploy.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import Nav from "../components/Nav.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import { site } from "../data/site";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
/** Path to the page, e.g. "/projects" — used for canonical + OG url. */
|
||||
path?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description = site.positioning,
|
||||
path = "/",
|
||||
} = Astro.props;
|
||||
|
||||
const pageTitle = title ? `${title} — ${site.name}` : `${site.name} · ${site.role}`;
|
||||
const canonical = new URL(path, site.url).href;
|
||||
const ogImage = new URL(site.ogImage, site.url).href;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={site.locale}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="canonical" href={canonical} />
|
||||
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="author" content={site.name} />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={pageTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:site_name" content={site.name} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={pageTitle} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
|
||||
<meta name="theme-color" content="#090c14" />
|
||||
|
||||
<!-- Pre-paint theme bootstrap: avoids a flash of the wrong theme.
|
||||
The ONE inline script on the site; hashed by Astro CSP in M4. -->
|
||||
<script is:inline>
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem("theme");
|
||||
var theme =
|
||||
stored ||
|
||||
(window.matchMedia("(prefers-color-scheme: light)").matches
|
||||
? "light"
|
||||
: "dark");
|
||||
document.documentElement.dataset.theme = theme;
|
||||
} catch (e) {
|
||||
document.documentElement.dataset.theme = "dark";
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<Nav />
|
||||
<main id="main">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
<!-- Reveal-on-scroll: progressive enhancement, motion-aware. -->
|
||||
<script>
|
||||
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
const targets = document.querySelectorAll<HTMLElement>("[data-reveal]");
|
||||
if (!reduce && "IntersectionObserver" in window) {
|
||||
targets.forEach((el) => el.classList.add("reveal"));
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("is-visible");
|
||||
io.unobserve(entry.target);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: "0px 0px -10% 0px", threshold: 0.1 },
|
||||
);
|
||||
targets.forEach((el) => io.observe(el));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user