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