diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/.prettierignore b/.prettierignore index f2fa146..50c462b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,9 @@ node_modules package-lock.json +# Generated by build.js, written pre-formatted (Prettier has no XML parser). +sitemap.xml + # Generated favicon assets (created by an external tool, not hand-maintained). images diff --git a/_template.html b/_template.html index d4c0d66..8a79965 100644 --- a/_template.html +++ b/_template.html @@ -4,7 +4,7 @@ - workman.tech — {{title}} + {{title}} · workman.tech @@ -33,6 +33,7 @@ /> + {{json_ld}} diff --git a/_theme_template.html b/_theme_template.html index 54bdca6..b8efbd0 100644 --- a/_theme_template.html +++ b/_theme_template.html @@ -4,15 +4,15 @@ - workman.tech — {{theme_display}} + {{theme_display}} · workman.tech - + - + block. JSON embedded in , so we escape <, >, & to their \uXXXX +// forms after stringifying. This is deliberately NOT escapeHtml — that would +// corrupt the JSON (e.g. turn " into "); JSON.stringify already handles +// escaping of the values themselves. +function jsonLdScript(obj) { + const json = JSON.stringify(obj, null, 2) + .replace(//g, "\\u003e") + .replace(/&/g, "\\u0026"); + return ``; +} + +// Per-post BlogPosting structured data. author/publisher reference the Person +// node defined on the homepage by @id, and isPartOf references the WebSite, +// so the whole site resolves to one author entity. Dates come from frontmatter +// only (keeps the build deterministic — same rule as the sitemap). +function renderPostJsonLd(post, description) { + const url = canonicalUrlForPost(post); + const obj = { + "@context": "https://schema.org", + "@type": "BlogPosting", + headline: post.title, + description, + datePublished: post.date, + dateModified: post.date, + url, + mainEntityOfPage: url, + inLanguage: "en", + }; + if (post.tags.length > 0) obj.keywords = post.tags; + obj.isPartOf = { "@id": WEBSITE_ID }; + obj.author = { "@type": "Person", "@id": PERSON_ID, name: "Ryan Workman" }; + obj.publisher = { "@id": PERSON_ID }; + return jsonLdScript(obj); +} + function renderPost(template, post) { const description = post.description || DEFAULT_DESCRIPTION; const content = renderPostBody(post); @@ -347,6 +390,7 @@ function renderPost(template, post) { theme_slug: post.themeSlug, tags_display: post.tags.map(escapeHtml).join(", "), theme_and_tags_line: themeAndTagsLine(post), + json_ld: renderPostJsonLd(post, description), content, }); } @@ -455,6 +499,51 @@ async function regenerateIndex(byTheme) { } } +// ---------- sitemap ---------- + +// Build sitemap.xml: the homepage, every theme landing page, and every post. +// is derived only from frontmatter dates (never build time), so the +// output is byte-stable and CI's "git clean after build" check holds. Not run +// through Prettier — Prettier core has no XML parser — so the formatting here +// is the committed formatting. URL order is deterministic: homepage, then +// themes in the byTheme map's (alphabetical) order, each landing page followed +// by its posts. +function renderSitemap(byTheme) { + const entries = []; + let newestOverall = null; + for (const posts of byTheme.values()) { + for (const p of posts) { + if (!newestOverall || p.date > newestOverall) newestOverall = p.date; + } + } + entries.push({ loc: `${SITE_ORIGIN}/`, lastmod: newestOverall }); + for (const [themeSlug, posts] of byTheme) { + let newestInTheme = null; + for (const p of posts) { + if (!newestInTheme || p.date > newestInTheme) newestInTheme = p.date; + } + entries.push({ + loc: canonicalUrlForTheme(themeSlug), + lastmod: newestInTheme, + }); + for (const p of posts) { + entries.push({ loc: canonicalUrlForPost(p), lastmod: p.date }); + } + } + const body = entries + .map(({ loc, lastmod }) => { + const lastmodLine = lastmod ? `\n ${lastmod}` : ""; + return ` \n ${escapeHtml(loc)}${lastmodLine}\n `; + }) + .join("\n"); + return ( + `\n` + + `\n` + + `${body}\n` + + `\n` + ); +} + // ---------- cleaning ---------- // Reserved top-level entries that we must never treat as generated theme @@ -514,7 +603,8 @@ async function cleanGeneratedDirs() { for (const name of generated) { fs.rmSync(path.join(ROOT, name), { recursive: true, force: true }); } - // Also reset the index post-list region. + // Also remove the generated sitemap and reset the index post-list region. + fs.rmSync(SITEMAP_PATH, { force: true }); if (fs.existsSync(INDEX_PATH)) { await regenerateIndex(new Map()); } @@ -577,6 +667,7 @@ async function build() { } await regenerateIndex(byTheme); + fs.writeFileSync(SITEMAP_PATH, renderSitemap(byTheme)); const themeCount = byTheme.size; console.log( diff --git a/engineering/2026-06-03-the-repo-is-the-tracker.html b/engineering/2026-06-03-the-repo-is-the-tracker.html index a6dcec1..84e9862 100644 --- a/engineering/2026-06-03-the-repo-is-the-tracker.html +++ b/engineering/2026-06-03-the-repo-is-the-tracker.html @@ -5,8 +5,8 @@ - workman.tech — the repo is the tracker: solo dev project management in the - time of ai + the repo is the tracker: solo dev project management in the time of ai · + workman.tech + diff --git a/engineering/2026-06-12-building-an-oracle.html b/engineering/2026-06-12-building-an-oracle.html index d68710f..6f458b1 100644 --- a/engineering/2026-06-12-building-an-oracle.html +++ b/engineering/2026-06-12-building-an-oracle.html @@ -5,8 +5,8 @@ - workman.tech — building an oracle: a knowledge base of how my team - actually reviews code + building an oracle: a knowledge base of how my team actually reviews code + · workman.tech + diff --git a/engineering/index.html b/engineering/index.html index 3799e1c..5063d47 100644 --- a/engineering/index.html +++ b/engineering/index.html @@ -4,18 +4,18 @@ - workman.tech — Engineering + Engineering · workman.tech - + - + + diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..0a2ae48 --- /dev/null +++ b/robots.txt @@ -0,0 +1,17 @@ +# workman.tech crawl policy +# Goal: maximize discoverability, including AI/LLM crawlers. No actual content +# is disallowed; the Disallow lines just keep crawlers off the build +# scaffolding that .nojekyll now exposes. Per-bot control (e.g. allow +# Claude-SearchBot but block ClaudeBot training) can be added later; today +# every bot is allowed. + +User-agent: * +Allow: / +Disallow: /_ +Disallow: /build.js +Disallow: /CLAUDE.md +Disallow: /README.md +Disallow: /package.json +Disallow: /package-lock.json + +Sitemap: https://workman.tech/sitemap.xml diff --git a/side-projects/2026-05-24-pokemon-card-scanner-in-one-night.html b/side-projects/2026-05-24-pokemon-card-scanner-in-one-night.html index ac5dcea..0ff21ca 100644 --- a/side-projects/2026-05-24-pokemon-card-scanner-in-one-night.html +++ b/side-projects/2026-05-24-pokemon-card-scanner-in-one-night.html @@ -5,8 +5,8 @@ - workman.tech — i built a pokémon card scanner in one night with claude - code + i built a pokémon card scanner in one night with claude code · + workman.tech + diff --git a/side-projects/index.html b/side-projects/index.html index 9ef1fe1..4c04c58 100644 --- a/side-projects/index.html +++ b/side-projects/index.html @@ -4,18 +4,18 @@ - workman.tech — Side Projects + Side Projects · workman.tech - + - + + + + https://workman.tech/ + 2026-06-15 + + + https://workman.tech/engineering/ + 2026-06-12 + + + https://workman.tech/engineering/2026-06-12-building-an-oracle.html + 2026-06-12 + + + https://workman.tech/engineering/2026-06-03-the-repo-is-the-tracker.html + 2026-06-03 + + + https://workman.tech/side-projects/ + 2026-05-24 + + + https://workman.tech/side-projects/2026-05-24-pokemon-card-scanner-in-one-night.html + 2026-05-24 + + + https://workman.tech/understanding-clients/ + 2026-06-15 + + + https://workman.tech/understanding-clients/2026-06-15-ai-didnt-break-the-laws-of-physics.html + 2026-06-15 + + diff --git a/understanding-clients/2026-06-15-ai-didnt-break-the-laws-of-physics.html b/understanding-clients/2026-06-15-ai-didnt-break-the-laws-of-physics.html index 273fae0..601a2fb 100644 --- a/understanding-clients/2026-06-15-ai-didnt-break-the-laws-of-physics.html +++ b/understanding-clients/2026-06-15-ai-didnt-break-the-laws-of-physics.html @@ -4,7 +4,7 @@ - workman.tech — ai didn't break the laws of physics + ai didn't break the laws of physics · workman.tech + diff --git a/understanding-clients/index.html b/understanding-clients/index.html index b07803f..5a7be35 100644 --- a/understanding-clients/index.html +++ b/understanding-clients/index.html @@ -4,7 +4,7 @@ - workman.tech — understanding clients in the age of ai + understanding clients in the age of ai · workman.tech