279 lines
8.3 KiB
Text
279 lines
8.3 KiB
Text
---
|
|
import BaseLayout from "~/layouts/BaseLayout.astro";
|
|
import Eyebrow from "~/components/atoms/Eyebrow.astro";
|
|
import Mark from "~/components/atoms/Mark.astro";
|
|
import PostRow from "~/components/molecules/PostRow.astro";
|
|
import YearHead from "~/components/molecules/YearHead.astro";
|
|
import InkArrow from "~/components/atoms/InkArrow.astro";
|
|
import BlogSearch from "~/components/molecules/BlogSearch.astro";
|
|
import { blog } from "~/lib/urls";
|
|
import { getCollection } from "astro:content";
|
|
|
|
const posts = await getCollection("posts", ({ data }) => !data.draft);
|
|
posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
|
|
|
const [featured, ...rest] = posts;
|
|
|
|
const dateFmtMonthYear = new Intl.DateTimeFormat("en-AU", { month: "short", year: "numeric" });
|
|
const dateFmtMonthYearLong = new Intl.DateTimeFormat("en-AU", { month: "long", year: "numeric" });
|
|
|
|
// Group the archive by year.
|
|
const byYear = new Map<number, typeof rest>();
|
|
for (const post of rest) {
|
|
const year = post.data.date.getFullYear();
|
|
const bucket = byYear.get(year) ?? [];
|
|
bucket.push(post);
|
|
byYear.set(year, bucket);
|
|
}
|
|
const yearGroups = Array.from(byYear.entries()).sort(([a], [b]) => b - a);
|
|
|
|
// Build the per-post `data-search` string at build time. We concatenate the
|
|
// searchable fields, lowercase, strip a small set of markdown punctuation, and
|
|
// collapse whitespace. Body text is the raw markdown from the content
|
|
// collection entry's `.body` property — MDX expressions that survive end up as
|
|
// literal text, which is harmless for substring matching.
|
|
function buildSearchText(post: (typeof posts)[number]): string {
|
|
const parts = [
|
|
post.data.title,
|
|
post.data.tag,
|
|
post.data.standfirst,
|
|
post.data.description,
|
|
post.body ?? "",
|
|
].filter(Boolean) as string[];
|
|
return parts
|
|
.join(" ")
|
|
.toLowerCase()
|
|
.replace(/[<>#*_`\[\]()]/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
const searchTextById = new Map<string, string>();
|
|
for (const post of posts) {
|
|
searchTextById.set(post.id, buildSearchText(post));
|
|
}
|
|
---
|
|
|
|
<BaseLayout
|
|
title="Writing — Josh Bairstow"
|
|
description="Short pieces on craft, the ocean, and the quiet discipline of making small software."
|
|
current="blog"
|
|
>
|
|
<header class="masthead">
|
|
<Mark kind="ringdots" width={72} top="-12px" right="-14px" rotate={-4} />
|
|
<div>
|
|
<Eyebrow>Journal · field notes</Eyebrow>
|
|
<h1 class="title">Writing<span style="color:var(--accent-deep)">.</span></h1>
|
|
<p class="count">{posts.length} entries · since 2021</p>
|
|
</div>
|
|
<p class="blurb">
|
|
Short pieces on craft, the ocean, and the quiet discipline of making small
|
|
software. Mostly written early, before the light is up.
|
|
</p>
|
|
</header>
|
|
|
|
<BlogSearch />
|
|
|
|
{
|
|
featured && (
|
|
<article class="feature" data-featured data-search={searchTextById.get(featured.id) ?? ""}>
|
|
<div class="cutframe img">
|
|
<div class="grain" />
|
|
<img class="wm" src="/assets/mark-rings.svg" alt="" />
|
|
</div>
|
|
<div>
|
|
<div class="meta">
|
|
{featured.data.tag && <span class="tag">{featured.data.tag}</span>}
|
|
<span class="date">
|
|
{dateFmtMonthYearLong.format(featured.data.date)}
|
|
{featured.data.readingMinutes && ` · ${featured.data.readingMinutes} min`}
|
|
</span>
|
|
</div>
|
|
<h2>
|
|
<a href={blog(`/posts/${featured.id}/`)}>{featured.data.title}</a>
|
|
</h2>
|
|
{featured.data.standfirst && <p>{featured.data.standfirst}</p>}
|
|
<a class="read" href={blog(`/posts/${featured.id}/`)}>
|
|
<span class="u">Read the piece</span>
|
|
<InkArrow />
|
|
</a>
|
|
</div>
|
|
</article>
|
|
)
|
|
}
|
|
|
|
<section class="archive">
|
|
<Mark kind="dots" width={54} top="-14px" right="-10px" />
|
|
<Mark kind="concentric" width={40} bottom="-12px" left="-12px" rotate={6} />
|
|
{
|
|
yearGroups.map(([year, items]) => (
|
|
<>
|
|
<YearHead year={String(year)} count={items.length} />
|
|
{items.map((post) => (
|
|
<PostRow
|
|
date={dateFmtMonthYear.format(post.data.date)}
|
|
title={post.data.title}
|
|
href={blog(`/posts/${post.id}/`)}
|
|
deck={post.data.standfirst}
|
|
tag={post.data.tag}
|
|
searchText={searchTextById.get(post.id) ?? ""}
|
|
/>
|
|
))}
|
|
</>
|
|
))
|
|
}
|
|
</section>
|
|
|
|
<p class="empty" data-empty hidden aria-live="polite">
|
|
No entries match “<span data-empty-query></span>”.
|
|
</p>
|
|
</BaseLayout>
|
|
|
|
<style>
|
|
.masthead {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: 1.1fr 0.9fr;
|
|
gap: clamp(30px, 6vw, 80px);
|
|
align-items: end;
|
|
padding: clamp(40px, 8vh, 96px) 0 clamp(30px, 5vh, 52px);
|
|
}
|
|
.masthead .title {
|
|
font-family: var(--font-display);
|
|
font-weight: 500;
|
|
font-size: clamp(56px, 9vw, 132px);
|
|
line-height: 0.9;
|
|
letter-spacing: -0.03em;
|
|
margin: 12px 0 0;
|
|
color: var(--fg-on-dark-1);
|
|
}
|
|
:global(body:not(.dark)) .masthead .title { color: var(--fg-1); }
|
|
.masthead .blurb {
|
|
font-family: var(--font-body);
|
|
font-weight: 300;
|
|
font-size: clamp(15px, 1.4vw, 18px);
|
|
line-height: 1.62;
|
|
color: var(--fg-on-dark-2);
|
|
max-width: 38ch;
|
|
margin: 0 0 6px;
|
|
}
|
|
:global(body:not(.dark)) .masthead .blurb { color: var(--fg-2); }
|
|
.masthead .count {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
letter-spacing: 0.08em;
|
|
color: var(--fg-on-dark-3);
|
|
margin-top: 16px;
|
|
font-feature-settings: "onum" 1;
|
|
}
|
|
|
|
.feature {
|
|
display: grid;
|
|
grid-template-columns: 0.92fr 1.08fr;
|
|
gap: clamp(28px, 5vw, 64px);
|
|
align-items: center;
|
|
padding: clamp(26px, 4.5vh, 48px) 0;
|
|
border-top: 1px solid var(--hairline-on-dark);
|
|
border-bottom: 1px solid var(--hairline-on-dark);
|
|
}
|
|
:global(body:not(.dark)) .feature { border-color: var(--hairline); }
|
|
.feature .img { aspect-ratio: 5 / 4; }
|
|
.meta {
|
|
display: flex;
|
|
gap: 14px;
|
|
align-items: center;
|
|
margin-bottom: 18px;
|
|
}
|
|
.tag {
|
|
font-family: var(--font-body);
|
|
font-weight: 500;
|
|
font-size: 11px;
|
|
letter-spacing: 0.2em;
|
|
text-transform: uppercase;
|
|
color: var(--accent);
|
|
white-space: nowrap;
|
|
}
|
|
:global(body:not(.dark)) .tag { color: var(--accent-deep); }
|
|
.date {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
color: var(--fg-on-dark-3);
|
|
font-feature-settings: "onum" 1;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
:global(body:not(.dark)) .date { color: var(--fg-3); }
|
|
.feature h2 {
|
|
font-family: var(--font-display);
|
|
font-weight: 500;
|
|
font-size: clamp(28px, 3.4vw, 46px);
|
|
line-height: 1.04;
|
|
letter-spacing: -0.02em;
|
|
margin: 0;
|
|
color: var(--fg-on-dark-1);
|
|
text-wrap: balance;
|
|
}
|
|
:global(body:not(.dark)) .feature h2 { color: var(--fg-1); }
|
|
.feature h2 a { color: inherit; text-decoration: none; }
|
|
.feature p {
|
|
font-family: var(--font-body);
|
|
font-weight: 300;
|
|
font-size: 16.5px;
|
|
line-height: 1.62;
|
|
color: var(--fg-on-dark-2);
|
|
margin: 18px 0 0;
|
|
max-width: 48ch;
|
|
}
|
|
:global(body:not(.dark)) .feature p { color: var(--fg-2); }
|
|
.read {
|
|
display: inline-flex;
|
|
align-items: baseline;
|
|
gap: 10px;
|
|
margin-top: 24px;
|
|
font-family: var(--font-body);
|
|
font-weight: 500;
|
|
font-size: 13px;
|
|
letter-spacing: 0.04em;
|
|
text-decoration: none;
|
|
color: var(--fg-on-dark-1);
|
|
white-space: nowrap;
|
|
}
|
|
:global(body:not(.dark)) .read { color: var(--fg-1); }
|
|
.read .u {
|
|
position: relative;
|
|
padding-bottom: 3px;
|
|
}
|
|
.read .u::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
bottom: 0;
|
|
height: 1px;
|
|
width: 100%;
|
|
background: var(--accent);
|
|
}
|
|
|
|
.archive {
|
|
position: relative;
|
|
padding: clamp(34px, 6vh, 72px) 0 0;
|
|
}
|
|
|
|
@media (max-width: 800px) {
|
|
.masthead,
|
|
.feature { grid-template-columns: 1fr; }
|
|
.feature .img { order: -1; aspect-ratio: 16 / 9; }
|
|
}
|
|
|
|
/* :global so the runtime-toggled class reaches PostRow and YearHead roots,
|
|
which carry their own component scope hashes — not this page's. */
|
|
:global(.is-hidden) { display: none !important; }
|
|
|
|
.empty {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
letter-spacing: 0.08em;
|
|
color: var(--fg-on-dark-3);
|
|
margin: clamp(28px, 5vh, 52px) 0 0;
|
|
font-feature-settings: "onum" 1;
|
|
}
|
|
:global(body:not(.dark)) .empty { color: var(--fg-3); }
|
|
</style>
|