meowpress 0.3.0-alpha.2 → 0.3.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/site.js CHANGED
@@ -1,141 +1,114 @@
1
1
  import { themeAttributes } from "meowcss";
2
+ import { createRuntime, Fragment, h } from "meowjsx";
3
+ import { renderToHtmlWithRuntime } from "meowjsx/web";
4
+ import { Button, Container, Entry, EntryList, Inline, MetaRow, Page, PageHeader, Prose, Section, Tag, Text, createMeowUI, } from "meowtail";
2
5
  import { blogTheme } from "./theme.js";
3
- import { Fragment, h } from "meowjsx/runtime";
4
- import { renderToHtml } from "meowjsx/web";
5
6
  import { withoutLeadingHeading } from "./content.js";
6
7
  import { DEFAULT_SITE } from "./identity.js";
7
8
  import { sitePageAgentUrl } from "./pages.js";
8
9
  import { layoutSkillGraph } from "./skill-graph.js";
9
10
  /** The name of the theme stylesheet the package writes by default. */
10
11
  export const DEFAULT_THEME_STYLESHEET = "blog-theme.css";
11
- /** The href of the theme stylesheet for a page at a given depth. */
12
12
  function styleHref(depth, stylesheet) {
13
13
  return `${"../".repeat(depth)}${stylesheet}`;
14
14
  }
15
- /** The number of directories a slug puts a page below its section. */
16
15
  function slugDepth(slug) {
17
16
  return slug.split("/").filter(Boolean).length;
18
17
  }
19
18
  function SiteHeader({ activePath, site }) {
20
- return (h("header", { className: "site" },
19
+ const links = [
20
+ ["/posts/", "Posts"],
21
+ ["/thoughts/", "Thoughts"],
22
+ ["/skills/", "Skills"],
23
+ ["/about/", "About"],
24
+ ];
25
+ return (h("header", { className: "site-header" },
21
26
  h("a", { className: "site-name", href: "/" }, site.name),
22
- h("nav", { className: "site-nav", "aria-label": "Primary navigation" },
23
- h("a", { className: activePath === "/skills/" ? "active" : "", href: "/skills/" }, "Skills"),
24
- h("a", { className: activePath === "/about/" ? "active" : "", href: "/about/" }, "About"))));
27
+ h("nav", { className: "site-nav", "aria-label": "Primary navigation" }, links.map(([href, label]) => (h("a", { href: href, "aria-current": activePath === href ? "page" : undefined }, label))))));
25
28
  }
26
29
  function SiteFooter({ site }) {
27
- return (h("footer", { className: "site" },
28
- h("span", { className: "brand" },
30
+ return (h("footer", { className: "site-footer" },
31
+ h(Text, { variant: "caption" },
29
32
  "\u00A9 ",
30
33
  new Date().getFullYear(),
31
34
  " ",
32
35
  site.name),
33
- h("span", null,
36
+ h(Inline, { gap: "compact" },
34
37
  h("a", { href: "/llm.txt" }, "llm.txt"),
35
- " \u00B7 ",
36
38
  h("a", { href: "/agents.md" }, "agents.md"))));
37
39
  }
38
- function Document({ title, description, activePath, styleHref, children, theme, site, }) {
39
- const documentTitle = title === site.name ? site.name : `${title} · ${site.name}`;
40
- return (h("html", { lang: "en", ...themeAttributes(theme) },
40
+ function Document({ title, description, activePath, styleHref, baseHref, wide, children, theme, site, }) {
41
+ return (h("html", { lang: "en", "data-meowui-page": "", ...themeAttributes(theme) },
41
42
  h("head", null,
42
43
  h("meta", { charSet: "UTF-8" }),
43
44
  h("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
44
45
  h("meta", { name: "description", content: description ?? site.description }),
45
- h("meta", { name: "theme-color", media: "(prefers-color-scheme: light)", content: "#faf8f3" }),
46
- h("meta", { name: "theme-color", media: "(prefers-color-scheme: dark)", content: "#191816" }),
46
+ baseHref ? h("base", { href: baseHref }) : null,
47
47
  h("link", { rel: "stylesheet", href: styleHref }),
48
- h("title", null, documentTitle)),
48
+ h("title", null, title === site.name ? site.name : `${title} · ${site.name}`)),
49
49
  h("body", { "data-site": site.id },
50
- h("div", { className: "container" },
51
- h(SiteHeader, { activePath: activePath, site: site }),
52
- children,
53
- h(SiteFooter, { site: site })))));
50
+ h("a", { className: "skip-link", href: "#main-content" }, "Skip to content"),
51
+ h("div", { className: "site-shell" },
52
+ h(Container, { width: wide ? "wide" : "content" },
53
+ h(SiteHeader, { activePath: activePath, site: site }),
54
+ h("main", { id: "main-content", tabIndex: -1 }, children),
55
+ h(SiteFooter, { site: site }))))));
54
56
  }
55
57
  function pageHtml(props) {
56
- return `<!doctype html>\n${renderToHtml(h(Document, { ...props }))}`;
58
+ // Static rendering needs the same part/option adapter as a browser mount. No hydration is needed.
59
+ const runtime = createRuntime({
60
+ plugins: [
61
+ createMeowUI({
62
+ components: [
63
+ Button,
64
+ Container,
65
+ Entry,
66
+ EntryList,
67
+ Inline,
68
+ MetaRow,
69
+ Page,
70
+ PageHeader,
71
+ Prose,
72
+ Section,
73
+ Tag,
74
+ Text,
75
+ ],
76
+ }),
77
+ ],
78
+ });
79
+ return `<!doctype html>\n${renderToHtmlWithRuntime(runtime, h(Document, { ...props }))}`;
57
80
  }
58
81
  function dateLabel(dateKey) {
59
- if (dateKey === "undated")
82
+ const date = new Date(`${dateKey}T00:00:00Z`);
83
+ if (Number.isNaN(date.getTime()))
60
84
  return { date: "Undated", weekday: "" };
61
- const date = new Date(`${dateKey}T00:00:00+08:00`);
62
85
  return {
63
86
  date: new Intl.DateTimeFormat("en-US", {
64
- timeZone: "Asia/Shanghai",
87
+ timeZone: "UTC",
65
88
  month: "long",
66
89
  day: "numeric",
67
90
  }).format(date),
68
- weekday: new Intl.DateTimeFormat("en-US", {
69
- timeZone: "Asia/Shanghai",
70
- weekday: "short",
71
- year: "numeric",
72
- }).format(date),
91
+ weekday: `${date.getUTCFullYear()} ${new Intl.DateTimeFormat("en-US", { timeZone: "UTC", weekday: "short" }).format(date)}`,
73
92
  };
74
93
  }
75
94
  function itemUrl(item) {
76
95
  return `/${item.kind}s/${item.slug}/`;
77
96
  }
78
- function Hero({ image }) {
79
- return (h("section", { className: "hero", "aria-labelledby": "hero-title" },
80
- h("div", { className: "hero-copy" },
81
- h("h1", { id: "hero-title" },
82
- "Ten thousand years is too long;",
83
- h("br", null),
84
- h("em", null, "only this day is true."))),
85
- h("div", { className: "hero-work", "aria-hidden": "true" },
86
- h("svg", { className: "hero-doodle", viewBox: "0 0 260 190" },
87
- h("path", { className: "doodle-loop", d: "M24 96C20 45 58 18 122 20c72 2 116 33 114 84-2 48-48 70-113 68-65-2-96-27-99-76Z" }),
88
- h("circle", { className: "doodle-sun", cx: "194", cy: "55", r: "27" }),
89
- h("path", { className: "doodle-page", d: "M47 100c28-9 57-5 83 11v49c-24-13-52-16-83-7Z" }),
90
- h("path", { className: "doodle-page", d: "M130 111c24-15 52-19 82-9v51c-31-7-58-4-82 7Z" }),
91
- h("path", { className: "doodle-spine", d: "M130 111v49" }),
92
- h("path", { className: "doodle-writing", d: "M61 118c18-3 36-1 53 5M61 132c17-2 32 0 46 5M148 119c17-4 33-5 50-1M148 133c14-3 29-3 44 0" }),
93
- h("circle", { className: "doodle-dot", cx: "41", cy: "61", r: "5" }),
94
- h("path", { className: "doodle-spark", d: "M72 44v12M66 50h12" })),
95
- image ? h("img", { className: "hero-image", src: image, alt: "", decoding: "async" }) : null)));
96
- }
97
- function DoodleGithubIcon() {
98
- return (h("svg", { className: "github-icon", viewBox: "0 0 48 48", "aria-hidden": "true" },
99
- h("path", { className: "github-icon-ring", d: "M24 3.8C11.7 3.5 4.2 11.2 4.6 24.1c.3 12.1 7.8 20.1 19.6 20 12.2-.1 19.4-7.7 19.2-20C43.2 11.7 36.2 4.1 24 3.8Z" }),
100
- h("path", { className: "github-icon-cat", d: "M14.3 18.8c-.7-2.4-.3-5.1.8-7.1 3.2.1 5.4 1.3 6.6 2.2 1.5-.4 3.1-.6 4.8-.4 1.4-1 3.7-1.9 6.7-1.8 1 2.2 1.3 4.9.5 7.1 1.5 1.8 2.2 4.1 2 6.8-.4 5.2-3.2 8.2-8.2 8.8.8.7 1.3 1.9 1.3 3.8v4.3c-1.5.4-3.1.6-4.8.6-1.4 0-2.8-.1-4.2-.5v-3.4c0-1.8.5-3.1 1.4-4-4.8-.6-7.7-3.5-8.2-8.7-.3-2.7.4-5 1.3-6.7Z" }),
101
- h("path", { className: "github-icon-face", d: "M20.2 24.3h.1M29.2 24.3h.1M21.2 28.9c2.2 1.4 4.5 1.4 6.8 0" })));
102
- }
103
- function DoodleExternalArrow() {
104
- return (h("svg", { className: "external-arrow", viewBox: "0 0 28 28", "aria-hidden": "true" },
105
- h("path", { d: "M7 21 21 7M12 7h9v9" })));
106
- }
107
97
  function Tags({ item }) {
108
- if (item.tags.length === 0)
109
- return null;
110
- return (h("div", { className: "tags" }, item.tags.map((tag) => (h("span", null,
111
- "#",
112
- tag)))));
98
+ return item.tags.length ? (h(Inline, { gap: "compact" }, item.tags.map((tag) => (h(Tag, null, tag))))) : null;
113
99
  }
114
100
  function ThoughtCard({ item, linked = false }) {
115
- const body = (h(Fragment, null,
116
- h("div", { className: "time" },
117
- h("span", { className: "t" }, item.timeLabel || item.dateKey),
118
- " \u00B7 Thought"),
119
- h("div", { className: "body", dangerouslySetInnerHTML: { __html: withoutLeadingHeading(item.html) } }),
101
+ return (h(Entry, { className: "thought", "data-kind": "thought", title: item.title, href: linked ? itemUrl(item) : undefined, eyebrow: `${item.timeLabel || item.dateKey} · Thought` },
102
+ h(Prose, { dangerouslySetInnerHTML: { __html: withoutLeadingHeading(item.html) } }),
120
103
  h(Tags, { item: item })));
121
- if (!linked)
122
- return (h("article", { className: "thought", "data-kind": "thought" }, body));
123
- return (h("article", { className: "thought", "data-kind": "thought" },
124
- h("a", { className: "thought-link", href: itemUrl(item) }, body)));
125
104
  }
126
105
  function PostCard({ item }) {
127
106
  const category = typeof item.metadata.category === "string" ? item.metadata.category : "Post";
128
- return (h("a", { className: "post-card", href: itemUrl(item), "data-kind": "post" },
129
- h("div", { className: "card-top" },
130
- h("span", { className: "kind" }, category),
131
- h("span", { className: "meta" },
132
- "About ",
133
- item.readingMinutes,
134
- " min \u00B7 ",
135
- item.characterCount,
136
- " characters")),
137
- h("h3", null, item.title),
138
- h("p", { className: "excerpt" }, item.excerpt || "This post has no summary yet."),
107
+ return (h(Entry, { size: "feature", "data-kind": "post", title: item.title, href: itemUrl(item), eyebrow: category, description: item.excerpt || "This post has no summary yet.", meta: h(MetaRow, { items: [
108
+ { label: item.dateKey },
109
+ { label: `${item.readingMinutes} min read` },
110
+ { label: `${item.characterCount} characters` },
111
+ ] }) },
139
112
  h(Tags, { item: item })));
140
113
  }
141
114
  function groupByDate(items) {
@@ -147,108 +120,34 @@ function groupByDate(items) {
147
120
  }
148
121
  return groups;
149
122
  }
150
- function FeedDecor({ image }) {
151
- return (h("div", { className: "feed-decor", "aria-hidden": "true" },
152
- h("svg", { className: "yarn-doodle", viewBox: "0 0 88 70" },
153
- h("path", { className: "yarn-tail", d: "M42 48c-10 7-17 4-22 9-5 4-2 9 4 8 8-1 11-8 18-6 6 1 10 6 16 4" }),
154
- h("circle", { className: "yarn-fill", cx: "48", cy: "34", r: "22" }),
155
- h("path", { className: "yarn-line", d: "M31 23c11 1 22 7 34 18M28 34c12-7 24-9 39-5M34 49c7-14 16-24 28-31M28 42c13 1 24 5 33 12" })),
156
- h("svg", { className: "paw-trail", viewBox: "0 0 130 82" },
157
- h("g", { className: "paw-print", transform: "translate(12 54) rotate(-18)" },
158
- h("ellipse", { cx: "9", cy: "11", rx: "6", ry: "5" }),
159
- h("circle", { cx: "3", cy: "3", r: "2.5" }),
160
- h("circle", { cx: "9", cy: "1", r: "2.5" }),
161
- h("circle", { cx: "15", cy: "3", r: "2.5" })),
162
- h("g", { className: "paw-print", transform: "translate(45 37) rotate(8)" },
163
- h("ellipse", { cx: "9", cy: "11", rx: "6", ry: "5" }),
164
- h("circle", { cx: "3", cy: "3", r: "2.5" }),
165
- h("circle", { cx: "9", cy: "1", r: "2.5" }),
166
- h("circle", { cx: "15", cy: "3", r: "2.5" })),
167
- h("g", { className: "paw-print", transform: "translate(79 19) rotate(-12)" },
168
- h("ellipse", { cx: "9", cy: "11", rx: "6", ry: "5" }),
169
- h("circle", { cx: "3", cy: "3", r: "2.5" }),
170
- h("circle", { cx: "9", cy: "1", r: "2.5" }),
171
- h("circle", { cx: "15", cy: "3", r: "2.5" })),
172
- h("g", { className: "paw-print paw-print-accent", transform: "translate(111 5) rotate(11)" },
173
- h("ellipse", { cx: "7", cy: "9", rx: "5", ry: "4" }),
174
- h("circle", { cx: "2", cy: "2", r: "2" }),
175
- h("circle", { cx: "7", cy: "1", r: "2" }),
176
- h("circle", { cx: "12", cy: "2", r: "2" }))),
177
- image ? h("img", { className: "feed-image", src: image, alt: "", decoding: "async" }) : null));
178
- }
179
123
  function Feed({ items, image }) {
180
- if (items.length === 0) {
181
- return (h("main", { className: "feed feed-empty" },
182
- h("p", { className: "empty-state" }, "No public content yet."),
183
- h(FeedDecor, { image: image })));
184
- }
185
- return (h("main", { className: "feed" },
186
- [...groupByDate(items)].map(([dateKey, group]) => {
124
+ return (h("div", { className: "feed" },
125
+ items.length === 0 ? (h(Text, { variant: "secondary" }, "No public content yet.")) : ([...groupByDate(items)].map(([dateKey, group]) => {
187
126
  const label = dateLabel(dateKey);
188
- return (h("section", { className: "day" },
189
- h("h2", { className: "day-header" },
190
- label.date,
191
- " ",
192
- h("span", { className: "weekday" }, label.weekday)),
193
- group.map((item) => item.kind === "thought" ? (h(ThoughtCard, { item: item, linked: true })) : (h(PostCard, { item: item })))));
194
- }),
195
- h(FeedDecor, { image: image })));
127
+ return (h(Section, { className: "day", title: label.date, description: label.weekday },
128
+ h(EntryList, null, group.map((item) => item.kind === "thought" ? (h(ThoughtCard, { item: item, linked: true })) : (h(PostCard, { item: item }))))));
129
+ })),
130
+ image ? (h("img", { className: "feed-image", src: image, alt: "", loading: "lazy", decoding: "async" })) : null));
196
131
  }
197
132
  function Filter({ posts, thoughts }) {
198
- const total = posts + thoughts;
199
- return (h("div", { className: "tag-filter", "data-filter-group": "feed", role: "group", "aria-label": "Filter home feed" },
200
- h("button", { type: "button", "data-filter": "all", className: "active", "aria-pressed": "true" },
201
- "All",
202
- h("i", null, total)),
203
- h("button", { type: "button", "data-filter": "post", "aria-pressed": "false" },
204
- "Posts",
205
- h("i", null, posts)),
206
- h("button", { type: "button", "data-filter": "thought", "aria-pressed": "false" },
207
- "Thoughts",
208
- h("i", null, thoughts))));
133
+ return (h(Inline, { className: "feed-filter", gap: "compact", "data-filter-group": "feed", role: "group", "aria-label": "Filter home feed" }, [
134
+ ["all", "All", posts + thoughts],
135
+ ["post", "Posts", posts],
136
+ ["thought", "Thoughts", thoughts],
137
+ ].map(([value, label, count]) => (h(Button, { variant: "quiet", "data-filter": value, "aria-pressed": String(value === "all") },
138
+ label,
139
+ " ",
140
+ h("span", { className: "filter-count" }, count))))));
209
141
  }
210
142
  const FILTER_SCRIPT = `(() => {
211
143
  const links = document.querySelectorAll('[data-filter-group="feed"] [data-filter]');
212
144
  const items = document.querySelectorAll('.feed [data-kind]');
213
145
  const days = document.querySelectorAll('.feed .day');
214
- const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
215
- if ('IntersectionObserver' in window && !reduced) {
216
- const observer = new IntersectionObserver((entries) => {
217
- entries.forEach((entry) => {
218
- if (entry.isIntersecting) {
219
- entry.target.classList.add('in');
220
- observer.unobserve(entry.target);
221
- }
222
- });
223
- }, { threshold: 0.08 });
224
- days.forEach((day) => observer.observe(day));
225
- } else {
226
- days.forEach((day) => day.classList.add('in'));
227
- }
228
- links.forEach((link) => link.addEventListener('click', (event) => {
229
- event.preventDefault();
146
+ links.forEach((link) => link.addEventListener('click', () => {
230
147
  const filter = link.dataset.filter;
231
- links.forEach((item) => {
232
- const active = item === link;
233
- item.classList.toggle('active', active);
234
- item.setAttribute('aria-pressed', String(active));
235
- });
236
- items.forEach((item) => {
237
- item.style.display = filter === 'all' || item.dataset.kind === filter ? '' : 'none';
238
- });
239
- days.forEach((day) => {
240
- const visible = Array.from(day.querySelectorAll('[data-kind]'))
241
- .some((item) => item.style.display !== 'none');
242
- day.style.display = visible ? '' : 'none';
243
- if (!visible) day.classList.remove('in');
244
- });
245
- if (!reduced) {
246
- requestAnimationFrame(() => requestAnimationFrame(() => {
247
- days.forEach((day) => {
248
- if (day.style.display !== 'none') day.classList.add('in');
249
- });
250
- }));
251
- }
148
+ links.forEach((button) => button.setAttribute('aria-pressed', String(button === link)));
149
+ items.forEach((item) => { item.hidden = filter !== 'all' && item.dataset.kind !== filter; });
150
+ days.forEach((day) => { day.hidden = !Array.from(day.querySelectorAll('[data-kind]')).some((item) => !item.hidden); });
252
151
  }));
253
152
  })();`;
254
153
  export function homePage(items, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
@@ -257,29 +156,42 @@ export function homePage(items, theme = blogTheme, site = DEFAULT_SITE, styleshe
257
156
  theme,
258
157
  title: site.name,
259
158
  styleHref: styleHref(0, stylesheet),
260
- children: (h(Fragment, null,
261
- h(Hero, { image: site.images?.hero ? `./${site.images.hero}` : undefined }),
262
- h(Filter, { posts: items.filter((item) => item.kind === "post").length, thoughts: items.filter((item) => item.kind === "thought").length }),
159
+ children: (h(Page, { header: h("div", { className: "home-heading" },
160
+ h("div", { className: "home-heading-copy" },
161
+ h(Text, { variant: "eyebrow" }, "Journal"),
162
+ h(PageHeader, { title: site.name, description: site.description })),
163
+ site.images?.hero ? (h("img", { className: "hero-image", src: `./${site.images.hero}`, alt: "", decoding: "async" })) : null), toolbar: h(Filter, { posts: items.filter((item) => item.kind === "post").length, thoughts: items.filter((item) => item.kind === "thought").length }) },
263
164
  h(Feed, { items: items, image: site.images?.feed ? `./${site.images.feed}` : undefined }),
264
165
  h("script", { dangerouslySetInnerHTML: { __html: FILTER_SCRIPT } }))),
265
166
  });
266
167
  }
267
168
  export function listPage(kind, items, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
268
169
  const isPost = kind === "post";
170
+ const title = isPost ? "Posts" : "Thoughts";
269
171
  return pageHtml({
270
172
  site,
271
173
  theme,
272
- title: isPost ? "Posts" : "Thoughts",
174
+ title,
273
175
  activePath: isPost ? "/posts/" : "/thoughts/",
274
176
  styleHref: styleHref(1, stylesheet),
275
- children: (h("main", { className: "listing" },
276
- h("header", { className: "listing-head" },
277
- h("span", { className: "kind" }, isPost ? "Posts" : "Thoughts"),
278
- h("h1", null, isPost ? "Posts" : "Thoughts"),
279
- h("p", null, isPost
280
- ? "Complete arguments grown from fragments."
281
- : "Short, trimmed thoughts ready to show as they are.")),
282
- h("div", { className: isPost ? "post-list" : "thought-list" }, items.length === 0 ? (h("p", { className: "empty-state" }, "No public content yet.")) : (items.map((item) => isPost ? h(PostCard, { item: item }) : h(ThoughtCard, { item: item, linked: true })))))),
177
+ children: (h(Page, { title: title, eyebrow: "Archive", description: isPost
178
+ ? "Complete arguments grown from fragments."
179
+ : "Short, trimmed thoughts ready to show as they are." }, items.length ? (h(EntryList, null, items.map((item) => isPost ? h(PostCard, { item: item }) : h(ThoughtCard, { item: item, linked: true })))) : (h(Text, { variant: "secondary" }, "No public content yet.")))),
180
+ });
181
+ }
182
+ export function notFoundPage(copy, theme, site, stylesheet) {
183
+ const title = copy.title ?? "Page not found";
184
+ const message = copy.message ?? "This page may have moved or been removed.";
185
+ return pageHtml({
186
+ site,
187
+ theme,
188
+ title,
189
+ description: message,
190
+ styleHref: styleHref(0, stylesheet),
191
+ baseHref: "/",
192
+ children: (h(Page, { eyebrow: "404", title: title, description: message },
193
+ h(Prose, null,
194
+ h("a", { href: "/" }, copy.homeLabel ?? "Back to home")))),
283
195
  });
284
196
  }
285
197
  function PostNavigation({ item, posts }) {
@@ -287,22 +199,13 @@ function PostNavigation({ item, posts }) {
287
199
  const older = index >= 0 ? posts[index + 1] : undefined;
288
200
  const newer = index > 0 ? posts[index - 1] : undefined;
289
201
  return (h("nav", { className: "post-nav", "aria-label": "Post navigation" },
290
- older ? (h("a", { href: itemUrl(older) },
291
- h("span", { className: "label" }, "\u2190 Previous post (older)"),
292
- older.title)) : (h("span", null)),
293
- newer ? (h("a", { className: "next", href: itemUrl(newer) },
294
- h("span", { className: "label" }, "Next post (newer) \u2192"),
295
- newer.title)) : (h("span", { className: "next" },
296
- h("span", { className: "label" }, "Next post (newer) \u2192"),
297
- "Nothing newer \u2014 this is the latest post."))));
202
+ older ? (h(Entry, { eyebrow: "\u2190 Previous post (older)", title: older.title, href: itemUrl(older) })) : (h("span", null)),
203
+ newer ? (h(Entry, { eyebrow: "Next post (newer) \u2192", title: newer.title, href: itemUrl(newer) })) : (h(Text, { variant: "caption" }, "Nothing newer \u2014 this is the latest post."))));
298
204
  }
299
205
  function RelatedThoughts({ item, thoughts, }) {
300
206
  const related = thoughts.filter((thought) => thought.dateKey === item.dateKey).slice(0, 3);
301
- if (related.length === 0)
302
- return null;
303
- return (h("aside", { className: "related-thoughts" },
304
- h("h2", null, "Thoughts from the same day"),
305
- related.map((thought) => (h(ThoughtCard, { item: thought, linked: true })))));
207
+ return related.length ? (h(Section, { title: "Thoughts from the same day" },
208
+ h(EntryList, null, related.map((thought) => (h(ThoughtCard, { item: thought, linked: true })))))) : null;
306
209
  }
307
210
  export function postPage(item, posts, thoughts, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
308
211
  const category = typeof item.metadata.category === "string" ? item.metadata.category : "Post";
@@ -313,21 +216,16 @@ export function postPage(item, posts, thoughts, theme = blogTheme, site = DEFAUL
313
216
  description: item.excerpt,
314
217
  activePath: "/posts/",
315
218
  styleHref: styleHref(1 + slugDepth(item.slug), stylesheet),
316
- children: (h("main", null,
317
- h("article", null,
318
- h("header", { className: "post-head" },
319
- h("span", { className: "kind" }, category),
320
- h("h1", null, item.title),
321
- h("div", { className: "meta" },
322
- item.dateKey,
323
- " \u00B7 about ",
324
- item.readingMinutes,
325
- " min \u00B7 ",
326
- item.characterCount,
327
- " characters")),
328
- h("div", { className: "prose", dangerouslySetInnerHTML: { __html: item.html } }),
329
- h(RelatedThoughts, { item: item, thoughts: thoughts }),
330
- h(PostNavigation, { item: item, posts: posts })))),
219
+ children: (h(Page, { title: item.title, eyebrow: category },
220
+ h(MetaRow, { items: [
221
+ { label: item.dateKey },
222
+ { label: `${item.readingMinutes} min read` },
223
+ { label: `${item.characterCount} characters` },
224
+ ] }),
225
+ h(Prose, { as: "article", dangerouslySetInnerHTML: { __html: withoutLeadingHeading(item.html) } }),
226
+ h(Tags, { item: item }),
227
+ h(RelatedThoughts, { item: item, thoughts: thoughts }),
228
+ h(PostNavigation, { item: item, posts: posts }))),
331
229
  });
332
230
  }
333
231
  export function thoughtPage(item, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
@@ -338,15 +236,10 @@ export function thoughtPage(item, theme = blogTheme, site = DEFAULT_SITE, styles
338
236
  description: item.excerpt,
339
237
  activePath: "/thoughts/",
340
238
  styleHref: styleHref(1 + slugDepth(item.slug), stylesheet),
341
- children: (h("main", null,
342
- h("article", null,
343
- h("header", { className: "post-head" },
344
- h("span", { className: "kind" }, "Thought"),
345
- h("h1", null, item.title),
346
- h("div", { className: "meta" },
347
- item.dateKey,
348
- item.timeLabel ? ` · ${item.timeLabel}` : "")),
349
- h("div", { className: "prose", dangerouslySetInnerHTML: { __html: item.html } })))),
239
+ children: (h(Page, { title: item.title, eyebrow: "Thought" },
240
+ h(MetaRow, { items: [{ label: item.dateKey }, ...(item.timeLabel ? [{ label: item.timeLabel }] : [])] }),
241
+ h(Prose, { as: "article", dangerouslySetInnerHTML: { __html: withoutLeadingHeading(item.html) } }),
242
+ h(Tags, { item: item }))),
350
243
  });
351
244
  }
352
245
  export function aboutPage(theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
@@ -357,19 +250,13 @@ export function aboutPage(theme = blogTheme, site = DEFAULT_SITE, stylesheet = D
357
250
  title: "About",
358
251
  activePath: "/about/",
359
252
  styleHref: styleHref(1, stylesheet),
360
- children: (h("main", { className: "about" },
361
- h("div", { className: "avatar", "aria-hidden": "true" }, site.images?.about ? (h("img", { src: `../${site.images.about}`, alt: "", decoding: "async" })) : (h("span", { className: "avatar-initial" }, Array.from(site.name.trim())[0] ?? "M"))),
362
- about ? (h(Fragment, null,
363
- h("h1", null, about.heading),
364
- about.paragraphs.map((paragraph) => (h("p", null, paragraph))),
365
- about.links && about.links.length > 0 ? (h(Fragment, null,
366
- h("h2", null, about.linksHeading ?? "Find me"),
367
- about.links.map((link) => (h("a", { className: "github-link", href: link.href, target: "_blank", rel: "me noopener noreferrer" },
368
- h(DoodleGithubIcon, null),
369
- h("span", { className: "github-copy" },
370
- h("span", null, link.label),
371
- h("strong", null, link.handle)),
372
- h(DoodleExternalArrow, null)))))) : null)) : (h("h1", null, site.name)))),
253
+ children: (h(Page, { eyebrow: "About", title: about?.heading ?? site.name },
254
+ h("div", { className: "site-avatar", "aria-hidden": "true" }, site.images?.about ? (h("img", { src: `../${site.images.about}`, alt: "", decoding: "async" })) : (h("span", { className: "avatar-initial" }, Array.from(site.name.trim())[0] ?? "M"))),
255
+ about ? (h(Prose, null, about.paragraphs.map((paragraph) => (h("p", null, paragraph))))) : null,
256
+ about?.links?.length ? (h(Section, { title: about.linksHeading ?? "Find me" },
257
+ h(EntryList, null, about.links.map((link) => (h(Entry, { title: h("a", { href: link.href, target: "_blank", rel: "me noopener noreferrer" },
258
+ link.handle,
259
+ " \u2197"), eyebrow: link.label })))))) : null)),
373
260
  });
374
261
  }
375
262
  function skillUrl(skill) {
@@ -381,20 +268,12 @@ function skillInstallCommand(site, skill) {
381
268
  : skill.sourceBasePath;
382
269
  }
383
270
  function SkillCard({ skill, site }) {
384
- return (h("a", { className: `skill-card${skill.scope === "project" ? " project-skill" : ""}`, href: skillUrl(skill) },
385
- h("span", { className: "skill-card-source" },
386
- h("span", { className: "skill-card-path" }, skill.scope === "public" ? skillInstallCommand(site, skill) : skill.sourceBasePath),
387
- skill.scope === "project" ? (h("span", { className: "skill-scope-badge" }, "Project Skill")) : null),
388
- h("span", { className: "skill-card-main" },
389
- h("strong", null, skill.name),
390
- h("span", null, skill.description)),
391
- h("span", { className: "skill-card-foot" },
392
- h("span", null,
393
- skill.files.length,
394
- " Markdown files \u00B7 ",
395
- skill.relations.length,
396
- " references"),
397
- h("span", { className: "skill-open" }, "Open directory\u00A0 \u2192"))));
271
+ return (h(Entry, { href: skillUrl(skill), title: skill.name, description: skill.description, eyebrow: skill.scope === "project" ? "Project Skill" : "Public Skill", meta: h(MetaRow, { items: [
272
+ { label: `${skill.files.length} Markdown files` },
273
+ { label: `${skill.relations.length} references` },
274
+ ] }) },
275
+ h(Text, { variant: "caption" },
276
+ h("code", { className: "skill-command-text" }, skill.scope === "public" ? skillInstallCommand(site, skill) : skill.sourceBasePath))));
398
277
  }
399
278
  function skillTree(files) {
400
279
  const root = { name: "", children: [] };
@@ -438,7 +317,7 @@ function SkillGraphNodeView({ node }) {
438
317
  function SkillRelationGraph({ skill, direction }) {
439
318
  const graph = layoutSkillGraph(skill, direction);
440
319
  const markerId = `skill-arrow-${skill.slug}-${direction.toLowerCase()}`;
441
- return (h("svg", { className: `skill-graph-svg skill-graph-${direction.toLowerCase()}`, viewBox: `0 0 ${graph.width} ${graph.height}`, "aria-label": `${skill.name} file reference graph` },
320
+ return (h("svg", { className: `skill-graph-svg skill-graph-${direction.toLowerCase()}`, viewBox: `0 0 ${graph.width} ${graph.height}`, width: graph.width, height: graph.height, "aria-label": `${skill.name} file reference graph` },
442
321
  h("defs", null,
443
322
  h("marker", { id: markerId, viewBox: "0 0 10 10", refX: "8", refY: "5", markerWidth: "5", markerHeight: "5", orient: "auto-start-reverse" },
444
323
  h("path", { className: "skill-graph-arrow", d: "M 0 0 L 10 5 L 0 10 z" }))),
@@ -478,9 +357,8 @@ function SkillRelations({ skill }) {
478
357
  h("strong", null, missingNodes.length),
479
358
  " missing targets")),
480
359
  hasRelations ? (h(Fragment, null,
481
- h("div", { className: `skill-graph-canvas${compactGraph ? " compact" : ""}` },
482
- h(SkillRelationGraph, { skill: skill, direction: "LR" }),
483
- h(SkillRelationGraph, { skill: skill, direction: "TB" })),
360
+ h("div", { className: `skill-graph-canvas${compactGraph ? " compact" : ""}`, tabIndex: 0, role: "region", "aria-label": "Scrollable file reference graph" },
361
+ h(SkillRelationGraph, { skill: skill, direction: "LR" })),
484
362
  h("section", { className: "skill-reading-paths", "aria-labelledby": `${skill.slug}-paths` },
485
363
  h("h3", { id: `${skill.slug}-paths` }, "Where references happen"),
486
364
  h("ol", null, skill.relations.map((relation) => {
@@ -615,24 +493,11 @@ export function skillsIndexPage(skills, theme = blogTheme, site = DEFAULT_SITE,
615
493
  description: "Methods and working conventions that can be handed to an Agent.",
616
494
  activePath: "/skills/",
617
495
  styleHref: styleHref(1, stylesheet),
618
- children: (h("main", { className: "skills-index" },
619
- h("header", { className: "skills-head" },
620
- h("span", { className: "skill-path" }, "~/skills"),
621
- h("h1", null, "Skills"),
622
- h("p", null, "Methods shared publicly and usable directly by an Agent.")),
623
- h("section", { className: "skill-group", "aria-labelledby": "public-skills-heading" },
624
- h("header", { className: "skill-group-head" },
625
- h("h2", { id: "public-skills-heading" }, "Public Skills"),
626
- h("span", null, "Public content")),
627
- publicSkills.length > 0 ? (h("div", { className: "skill-list" }, publicSkills.map((skill) => (h(SkillCard, { skill: skill, site: site }))))) : (h("p", { className: "skill-group-empty" }, "Public Skills have no content yet."))),
628
- projectSkills.length > 0 ? (h("section", { className: "skill-group project-skills", "aria-labelledby": "project-skills-heading" },
629
- h("header", { className: "skill-group-head" },
630
- h("div", null,
631
- h("h2", { id: "project-skills-heading" }, "Project Skills"),
632
- h("small", null, "Development and content workflows used inside this project")),
633
- h("span", null, ".agents/skills/")),
634
- h("div", { className: "skill-list" }, projectSkills.map((skill) => (h(SkillCard, { skill: skill, site: site })))))) : null,
635
- h("p", { className: "skills-prototype-note" }, "The reference graph starts from SKILL.md and is generated at build time from Markdown links."))),
496
+ children: (h(Page, { eyebrow: "~/skills", title: "Skills", description: "Methods shared publicly and usable directly by an Agent." },
497
+ h(Section, { title: "Public Skills" }, publicSkills.length ? (h(EntryList, null, publicSkills.map((skill) => (h(SkillCard, { skill: skill, site: site }))))) : (h(Text, { variant: "secondary" }, "Public Skills have no content yet."))),
498
+ projectSkills.length ? (h(Section, { title: "Project Skills", description: "Development and content workflows used inside this project" },
499
+ h(EntryList, null, projectSkills.map((skill) => (h(SkillCard, { skill: skill, site: site })))))) : null,
500
+ h(Text, { variant: "caption" }, "The reference graph starts from SKILL.md and is generated at build time from Markdown links."))),
636
501
  });
637
502
  }
638
503
  export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE, stylesheet = DEFAULT_THEME_STYLESHEET) {
@@ -642,12 +507,13 @@ export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE, stylesh
642
507
  site,
643
508
  theme,
644
509
  title: skill.name,
510
+ wide: true,
645
511
  description: skill.description,
646
512
  activePath: "/skills/",
647
513
  styleHref: skill.scope === "public"
648
514
  ? styleHref(1 + slugDepth(skill.slug), stylesheet)
649
515
  : styleHref(2 + slugDepth(skill.slug), stylesheet),
650
- children: (h("main", { className: "skill-page" },
516
+ children: (h(Page, { width: "full", className: "skill-page", title: skill.name, eyebrow: "Skill", description: skill.description },
651
517
  h("a", { className: "skill-back", href: "/skills/" }, "\u2190 All Skills"),
652
518
  h("header", { className: "skill-browser-head" },
653
519
  h("span", { className: "skill-command" },
@@ -666,14 +532,17 @@ export function skillPage(skill, theme = blogTheme, site = DEFAULT_SITE, stylesh
666
532
  h("header", { className: "skill-viewer-bar" },
667
533
  h("span", { "data-current-file": true }, firstFile.path),
668
534
  h("span", null, "Markdown")),
669
- skill.files.map((file, index) => (h("article", { className: `skill-file-panel prose${index === 0 ? " active" : ""}`, "data-skill-panel": file.path, hidden: index !== 0, dangerouslySetInnerHTML: {
670
- __html: Bun.markdown.html(file.content, { tagFilter: true }),
535
+ skill.files.map((file, index) => (h(Prose, { as: "article", className: `skill-file-panel${index === 0 ? " active" : ""}`, "data-skill-panel": file.path, hidden: index !== 0, dangerouslySetInnerHTML: {
536
+ // The page owns h1; file headings nest under it without dropping any content.
537
+ __html: Bun.markdown
538
+ .html(file.content, { tagFilter: true })
539
+ .replace(/<(\/?)h([1-6])(\s[^>]*)?>/g, (_, closing, level, attributes = "") => `<${closing}h${Math.min(6, Number(level) + 1)}${attributes}>`),
671
540
  } }))))),
672
541
  h(SkillRelations, { skill: skill })),
673
542
  h("script", { dangerouslySetInnerHTML: { __html: SKILL_BROWSER_SCRIPT } }))),
674
543
  });
675
544
  }
676
- export function agentMarkdown(posts, thoughts, site = DEFAULT_SITE, pages) {
545
+ export function agentMarkdown(posts, thoughts, site = DEFAULT_SITE, pages, mappedSections = false) {
677
546
  const siteMap = pages
678
547
  ? pages.map((page) => `- [${page.title}](${sitePageAgentUrl(page)})`)
679
548
  : [
@@ -694,8 +563,12 @@ export function agentMarkdown(posts, thoughts, site = DEFAULT_SITE, pages) {
694
563
  "",
695
564
  "## Content model",
696
565
  "",
697
- `- \`${site.contentDirectory}/thoughts/\` stores trimmed, directly presentable short thoughts.`,
698
- `- \`${site.contentDirectory}/posts/\` stores full posts; files whose name or path segment starts with \`_\` are drafts and are not published or rendered.`,
566
+ mappedSections
567
+ ? "- Thoughts are trimmed, directly presentable notes."
568
+ : `- \`${site.contentDirectory}/thoughts/\` stores trimmed, directly presentable short thoughts.`,
569
+ mappedSections
570
+ ? "- Posts are full articles. Post files whose name or path segment starts with `_` are drafts."
571
+ : `- \`${site.contentDirectory}/posts/\` stores full posts; files whose name or path segment starts with \`_\` are drafts and are not published or rendered.`,
699
572
  "- Content files support `.md` and `.mdx`, and may start with YAML frontmatter.",
700
573
  "- Frontmatter currently has no required fields; `title`, `date`, `created_at`, `category` and `tags` are recognized by the site.",
701
574
  "",