diff --git a/src/components/media/Figure.astro b/src/components/media/Figure.astro
index 8c88706..4dcb9cb 100644
--- a/src/components/media/Figure.astro
+++ b/src/components/media/Figure.astro
@@ -55,17 +55,3 @@ const resolved = resolveImage(image)
)}
-
-
diff --git a/src/components/post/Header/ArticleHeader.astro b/src/components/post/Header/ArticleHeader.astro
index 8116953..a7c95a2 100644
--- a/src/components/post/Header/ArticleHeader.astro
+++ b/src/components/post/Header/ArticleHeader.astro
@@ -1,4 +1,6 @@
---
+import { toMilitaryDTG } from '@/lib/content/dates'
+import { trailOf } from '@/lib/site/navigation'
import type { Article } from '@/lib/site/types'
import Cover from './Cover.astro'
@@ -11,22 +13,18 @@ type Props = {
}
const { article } = Astro.props
+
+const crumbs = trailOf(article)
+const published = article.published
---
-
-
diff --git a/src/components/post/Header/Meta.astro b/src/components/post/Header/Meta.astro
index d93ec83..f11ddd6 100644
--- a/src/components/post/Header/Meta.astro
+++ b/src/components/post/Header/Meta.astro
@@ -48,70 +48,3 @@ const display = date && toMilitaryDTG(date)
)}
-
-
diff --git a/src/components/post/Header/Overline.astro b/src/components/post/Header/Overline.astro
index 44ff9b4..c9dbdab 100644
--- a/src/components/post/Header/Overline.astro
+++ b/src/components/post/Header/Overline.astro
@@ -1,40 +1,43 @@
---
-import { toMilitaryDTG } from '@/lib/content/dates'
-import { trailOf } from '@/lib/site/navigation'
-import type { Article } from '@/lib/site/types'
-
-type Props = {
- article: Article
+export type Crumb = {
+ title: string
+ path?: string
}
-const { article } = Astro.props
+type Props = {
+ crumbs: Crumb[]
+ stamp?: string
+ datetime?: string
+}
-const trail = trailOf(article)
+const { crumbs, stamp, datetime } = Astro.props
---
- {trail.length > 1 && (
+ {crumbs.length > 1 && (
)}
- {article.published ? (
-
@@ -57,7 +60,7 @@ const trail = trailOf(article)
display: flex;
gap: var(--size-4);
align-items: baseline;
- justify-content: space-between; /* breadcrumbs left, date right */
+ justify-content: space-between; /* breadcrumbs left, stamp right */
}
.breadcrumbs {
@@ -81,7 +84,7 @@ const trail = trailOf(article)
}
}
- .publicationdate {
+ .stamp {
font-weight: 700;
&::before {
diff --git a/src/components/post/Header/Title.astro b/src/components/post/Header/Title.astro
index 9fbc253..14b6249 100644
--- a/src/components/post/Header/Title.astro
+++ b/src/components/post/Header/Title.astro
@@ -11,28 +11,3 @@ const { title, subtitle } = Astro.props
{title}
{subtitle && {subtitle}
}
-
-
diff --git a/src/lib/site/directory.ts b/src/lib/site/directory.ts
new file mode 100644
index 0000000..8c2972d
--- /dev/null
+++ b/src/lib/site/directory.ts
@@ -0,0 +1,35 @@
+import type { DirectoryEntry, Site } from './types'
+
+const depthOf = (path: string) => path.split('/').filter(Boolean).length
+
+const byPath = (a: DirectoryEntry, b: DirectoryEntry) => a.path.localeCompare(b.path)
+
+export function directoryOf(site: Site): DirectoryEntry[] {
+ const articles = [...site.articles.values()]
+ .filter((article) => article.listed)
+ .map((article) => ({
+ title: article.title,
+ path: article.path,
+ collection: article.collection,
+ parent: article.parent?.title ?? null,
+ depth: depthOf(article.path),
+ }))
+
+ const pages = [...site.pages.values()].map((page) => ({
+ title: page.title,
+ path: page.path,
+ collection: page.collection,
+ parent: null,
+ depth: depthOf(page.path),
+ }))
+
+ const tags = [...site.tags.values()].map((tag) => ({
+ title: tag.title,
+ path: tag.path,
+ collection: tag.collection,
+ parent: null,
+ depth: depthOf(tag.path),
+ }))
+
+ return [...articles, ...pages, ...tags].sort(byPath)
+}
diff --git a/src/lib/site/rules.ts b/src/lib/site/rules.ts
index f6afafa..b9b2597 100644
--- a/src/lib/site/rules.ts
+++ b/src/lib/site/rules.ts
@@ -15,7 +15,7 @@ export const isListed = (status: Status, { drafts }: Visibility) =>
export const segmentOf = (entry: { title: string; route: { segment: string } }) =>
entry.route.segment || slugify(entry.title)
-const RESERVED = [TAGS, 'keystatic']
+const RESERVED = [TAGS, 'keystatic', 'api']
export function segmentProblem(segment: string, atRoot: boolean): string | null {
if (!segment) return 'The title has no URL-safe character. Set an URL segment.'
diff --git a/src/lib/site/types.ts b/src/lib/site/types.ts
index 9b4a039..5a852ad 100644
--- a/src/lib/site/types.ts
+++ b/src/lib/site/types.ts
@@ -37,3 +37,11 @@ export type Site = {
roots: Article[]
diagnostics: Diagnostic[]
}
+
+export type DirectoryEntry = {
+ title: string
+ path: string
+ collection: typeof ARTICLES | typeof PAGES | typeof TAGS
+ parent: string | null
+ depth: number
+}
diff --git a/src/pages/404.astro b/src/pages/404.astro
index 562a306..7ed37e4 100644
--- a/src/pages/404.astro
+++ b/src/pages/404.astro
@@ -1,27 +1,292 @@
---
+import Overline from '@/components/post/Header/Overline.astro'
import BaseLayout from '@/layouts/BaseLayout.astro'
+import { toMilitaryDTG } from '@/lib/content/dates'
import { getSite } from '@/lib/site'
const site = await getSite()
-const roots = site.roots
+
+const stamp = toMilitaryDTG(new Date())
+
+const disposition = [
+ {
+ title: 'Return to index',
+ path: '/',
+ },
+ ...site.roots.map((root) => ({
+ title: `Browser ${root.title}`,
+ path: root.path,
+ })),
+]
---
-
-
- 404
- Nothing here
-
- That page does not exist, or it moved. Try one of these, or
- start at the beginning.
-
- {roots.length > 0 && (
-
- )}
-
+
+
+
+
+ {stamp}
+
+
+
+
+
+ 404
+
+
Not on record
+
+ There is no entry under this address. It was never filed, or it has been struck from the
+ register.
+
+
+
+
+
01 ⟫ Requested
+
+
+
+ —
+
+
+
+
+
+
02 ⟫ Status
+
✖ Not found
+
+
+
+
03 ⟫ Checked
+
+ {stamp}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/api/site-index.json.ts b/src/pages/api/site-index.json.ts
new file mode 100644
index 0000000..d759cb7
--- /dev/null
+++ b/src/pages/api/site-index.json.ts
@@ -0,0 +1,6 @@
+import type { APIRoute } from 'astro'
+
+import { getSite } from '@/lib/site'
+import { directoryOf } from '@/lib/site/directory'
+
+export const GET: APIRoute = async () => Response.json(directoryOf(await getSite()))
diff --git a/src/styles/base/colors.css b/src/styles/base/colors.css
index 7abc95a..c3893e4 100644
--- a/src/styles/base/colors.css
+++ b/src/styles/base/colors.css
@@ -45,6 +45,7 @@
--color-text-quiet: var(--color-palette-ash);
--color-text-disabled: var(--color-palette-stone);
--color-text-inverse: var(--color-palette-paper);
+ --color-text-on-accent: var(--color-palette-black-ink);
/* == Surfaces == */
--color-surface-base: var(--color-palette-stock);
@@ -121,6 +122,7 @@
--color-text-quiet: var(--color-palette-sand);
--color-text-disabled: var(--color-palette-stone);
--color-text-inverse: var(--color-palette-black-ink);
+ --color-text-on-accent: var(--color-palette-black-ink);
/* == Surfaces: elevation still means lighter == */
--color-surface-base: var(--color-palette-black-ink);
diff --git a/src/styles/base/primitives.css b/src/styles/base/primitives.css
index 7af363e..92f6fa3 100644
--- a/src/styles/base/primitives.css
+++ b/src/styles/base/primitives.css
@@ -15,6 +15,7 @@
--type-8: 2.1875rem; /* 35px */
--type-9: 2.5rem; /* 40px */
--type-10: 2.75rem; /* 44px */
+ --type-slab: clamp(4rem, 9vw, 7.5rem);
/* == Line heights == */
--leading--3: 1.6;
diff --git a/src/styles/components/figure.css b/src/styles/components/figure.css
deleted file mode 100644
index 1e7dc81..0000000
--- a/src/styles/components/figure.css
+++ /dev/null
@@ -1,117 +0,0 @@
-@layer components {
- [class*="ratio--"] img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- object-position: center;
- }
-
- .ratio--21-9 img {
- aspect-ratio: 21 / 9;
- }
-
- .ratio--16-9 img {
- aspect-ratio: 16 / 9;
- }
-
- .ratio--3-2 img {
- aspect-ratio: 3 / 2;
- }
-
- .ratio--4-3 img {
- aspect-ratio: 4 / 3;
- }
-
- .ratio--1-1 img {
- aspect-ratio: 1 / 1;
- }
-
- .ratio--4-5 img {
- aspect-ratio: 4 / 5;
- }
-
- .effect--xeroxed {
- background-color: var(--color-surface-elevated-1);
- }
-
- .effect--xeroxed img {
- display: block;
- filter: grayscale(1) brightness(1.1) contrast(30);
- mix-blend-mode: multiply;
- }
-
- .effect--glitched {
- isolation: isolate;
- position: relative;
- }
-
- .effect--glitched img {
- display: block;
- filter: grayscale(1) contrast(1.1);
- }
-
- .effect--glitched::before,
- .effect--glitched::after {
- pointer-events: none;
- content: "";
-
- position: absolute;
- inset: 0;
-
- opacity: 0;
- background: var(--figure-src) center / cover no-repeat;
- mix-blend-mode: screen;
- }
-
- .effect--glitched:hover::before {
- opacity: 0.8;
- background-color: oklch(63% 0.2 25deg);
- background-blend-mode: multiply;
- animation: glitch-x 320ms steps(2, end) infinite;
- }
-
- .effect--glitched:hover::after {
- opacity: 0.8;
- background-color: oklch(70% 0.15 200deg);
- background-blend-mode: multiply;
- animation: glitch-x 260ms steps(3, end) infinite reverse;
- }
-
- .effect--glitched:hover img {
- filter: grayscale(0.15) contrast(1.3);
- }
-
- @keyframes glitch-x {
- 0% {
- transform: translate(0, 0);
- clip-path: inset(0 0 72% 0);
- }
-
- 25% {
- transform: translate(-3px, 1px);
- clip-path: inset(38% 0 34% 0);
- }
-
- 50% {
- transform: translate(3px, -1px);
- clip-path: inset(66% 0 8% 0);
- }
-
- 75% {
- transform: translate(-2px, 0);
- clip-path: inset(12% 0 60% 0);
- }
-
- 100% {
- transform: translate(0, 0);
- clip-path: inset(0 0 72% 0);
- }
- }
-
- @media (prefers-reduced-motion: reduce) {
- .effect--glitched:hover::before,
- .effect--glitched:hover::after {
- animation: none;
- }
- }
-}
diff --git a/src/styles/components/prose.css b/src/styles/components/prose.css
deleted file mode 100644
index 858e276..0000000
--- a/src/styles/components/prose.css
+++ /dev/null
@@ -1,6 +0,0 @@
-@layer components {
- .prose {
- @mixin typo_body;
- @mixin py var(--space-8);
- }
-}
diff --git a/src/styles/global.css b/src/styles/global.css
index 38ec98f..ce88068 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -3,8 +3,6 @@
@import url("base/foundation.css");
@import url("base/colors.css");
@import url("base/primitives.css");
-@import url("components/figure.css");
-@import url("components/prose.css");
@layer base {
body {