Add devlogs to taxonomy and activity feeds
Deploy production / deploy (push) Successful in 3s

This commit is contained in:
2026-08-11 23:57:15 -07:00
parent 75a8b910bc
commit 2964bdaf24
28 changed files with 189 additions and 37 deletions
+10 -5
View File
@@ -52,12 +52,17 @@ making changes; it is the authoritative operations and deployment runbook.
- Preserve normal links to dedicated tag pages as the no-JavaScript baseline.
Client-side filtering must remain a progressive enhancement with semantic
buttons, keyboard support, and an announced result count.
- Tag-directory counts and dedicated tag pages must include projects, blog
entries, and articles in the same discovery feed.
- Tag-directory counts and dedicated tag pages must include projects, their
devlog entries, blog entries, and articles in the same discovery feed.
- For remote projects, keep repository language labels and project-declared
technology labels separate from their derived controlled tag slugs. Approved
labels link to tag pages; unapproved labels remain visible, non-clickable, and
mustard-colored but do not affect tag feeds or counts.
- Apply that same controlled mapping to remote devlog topics. Approved topics
link to tag pages and populate `topic_tag_slugs`; unapproved topics remain
mustard labels and are excluded from discovery.
- Keep project devlog entries in the homepage Recent Activity feed alongside
projects, blog entries, and articles, ordered by publication date.
- When `README.md` changes, keep the README-derived example blog entry aligned
with the runbook content it demonstrates.
@@ -78,9 +83,9 @@ making changes; it is the authoritative operations and deployment runbook.
the relevant cache-busting version in the template and verify that generated
pages reference the new asset URL.
- For taxonomy changes, verify every entry tag has a matching `content/tags/`
record, tag counts match rendered project/blog/article usage, dedicated tag
URLs build, and listing filters work with valid, absent, and unknown query
values.
record, tag counts match rendered project/devlog/blog/article usage,
dedicated tag URLs build, and listing filters work with valid, absent, and
unknown query values.
- Run `git diff --check` and review the exact diff before committing.
- If a push was requested, verify the Gitea Action result, deployed revision,
and site health instead of treating a successful `git push` as completion.
+22 -16
View File
@@ -144,27 +144,30 @@ entry before its tag record exists. To rename or retire a tag, update every
referencing entry deliberately and verify that no old slug remains before
renaming or removing its record.
`/tags/` lists every approved tag and counts its usage across projects, blog
entries, and articles. Dedicated tag URLs combine matching records from all
three sections in one chronological discovery feed. Tag links are normal links
and work without JavaScript. On the Projects, Blog, and Articles listing pages,
`assets/static/tags.js` progressively adds instant filtering and keeps the
selected filter in the `tag` query parameter.
`/tags/` lists every approved tag and counts its usage across projects, project
devlog entries, blog entries, and articles. Dedicated tag URLs combine matching
records from all four content types in one chronological discovery feed. Tag
links are normal links and work without JavaScript. On the Projects, Blog, and
Articles listing pages, `assets/static/tags.js` progressively adds instant
filtering and keeps the selected filter in the `tag` query parameter.
Remote-project languages and declared technologies are normalized to slugs and
matched only against this controlled vocabulary. Their approved matches become
clickable tags and a deduplicated `taxonomy_tags` field used by filters, counts,
and tag pages. A label that has no approved record remains visible and
non-clickable in mustard, produces a build warning, and is excluded from tag
discovery. Remote data never creates a tag record automatically.
and tag pages. Devlog topics follow the same rule and populate derived
`topic_tag_slugs` used by tag feeds and counts. A label that has no approved
record remains visible and non-clickable in mustard, produces a build warning,
and is excluded from tag discovery. Remote data never creates a tag record
automatically.
After taxonomy or filtering changes:
1. Run `python scripts/build_with_projects.py --output-path build`.
2. Verify `/tags/`, at least one dedicated tag URL, and all three section listings.
3. Confirm tag counts cover projects, blog entries, and articles; every stored
entry slug has a matching tag record; and unapproved project labels are not
counted.
2. Verify `/tags/`, at least one dedicated tag URL, the three top-level section
listings, and a project devlog index.
3. Confirm tag counts cover projects, devlog entries, blog entries, and
articles; every stored entry slug has a matching tag record; and unapproved
remote labels are not counted.
4. Test filtering with JavaScript enabled and confirm tag links remain usable
without JavaScript.
5. Check keyboard operation plus desktop and narrow layouts.
@@ -194,7 +197,9 @@ The public routes are `/projects/thinkloom/`,
`/projects/thinkloom/devlog/`, and
`/projects/thinkloom/devlog/<entry-slug>/`. The project repository owns the
native Lektor records and approved images. This site owns their models,
templates, layout, repository-information card, and import policy.
templates, layout, repository-information card, and import policy. Imported
devlog entries also join the homepage Recent Activity feed, ordered with
projects, articles, and blog entries by publication date.
`configs/project-sources.ini` is the allowlist. Each source declares a stable
project ID, credential-free HTTPS Git URL, public web/API URL, and branch. Add
@@ -235,8 +240,9 @@ The project card combines repository-owned content with public provider and Git
metadata: source and README links, default branch, latest commit, license,
languages, open issues, stars, forks, latest release, imported revision, and
sync time. Approved language and technology labels link into the shared tag
taxonomy, allowing a project to lead visitors to related writing. API metadata
failure uses cached metadata and is identified in the
taxonomy, as do approved devlog topics, allowing either a project or a
development milestone to lead visitors to related writing. API metadata failure
uses cached metadata and is identified in the
sync status; it never causes an unvalidated new snapshot to replace a working
one.
@@ -161,19 +161,20 @@ entry before its tag record exists. To rename or retire a tag, update every
referencing entry deliberately and verify that no old slug remains before
renaming or removing its record.
`/tags/` lists every approved tag and counts its usage across projects, blog
entries, and articles. Dedicated tag URLs combine matching records from all
three sections. Tag links are normal links and work without JavaScript. On the
Projects, Blog, and Articles listing pages, `assets/static/tags.js`
progressively adds instant filtering and keeps the selected filter in the `tag`
query parameter.
`/tags/` lists every approved tag and counts its usage across projects, project
devlog entries, blog entries, and articles. Dedicated tag URLs combine matching
records from all four content types. Tag links are normal links and work without
JavaScript. On the Projects, Blog, and Articles listing pages,
`assets/static/tags.js` progressively adds instant filtering and keeps the
selected filter in the `tag` query parameter.
After taxonomy or filtering changes:
1. Run `python scripts/build_with_projects.py --output-path build`.
2. Verify `/tags/`, at least one dedicated tag URL, and all three section listings.
3. Confirm tag counts cover projects, blog entries, and articles and that every
stored entry slug has a matching tag record.
2. Verify `/tags/`, at least one dedicated tag URL, the three top-level section
listings, and a project devlog index.
3. Confirm tag counts cover projects, devlog entries, blog entries, and articles
and that every stored entry slug has a matching tag record.
4. Test filtering with JavaScript enabled and confirm tag links remain usable
without JavaScript.
5. Check keyboard operation plus desktop and narrow layouts.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: Architecture
---
summary: Product boundaries, system structure, and consequential technical design.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: Configuration
---
summary: User-editable and system configuration design and implementation.
+1 -1
View File
@@ -4,4 +4,4 @@ title: Tags
---
eyebrow: Taxonomy
---
summary: Browse the controlled vocabulary used across projects, blog entries, and articles.
summary: Browse the controlled vocabulary used across projects, devlogs, blog entries, and articles.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: Documentation
---
summary: Technical, operational, product, and user-facing documentation.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: Interface
---
summary: Interaction models, workspace layout, and user-interface design.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: JSON Schema
---
summary: Machine-readable JSON contracts, validation, fixtures, and compatibility.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: Models
---
summary: AI model integration, selection, configuration, and behavior.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: MVP
---
summary: Minimum viable product milestones and end-to-end prototypes.
@@ -0,0 +1,5 @@
_model: tag
---
title: OpenAI Build Week
---
summary: Projects, milestones, and releases developed during OpenAI Build Week.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: Privacy
---
summary: Data minimization, local ownership, credential handling, and private operation.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: Prompts
---
summary: Model prompt design, configuration, evaluation, and user control.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: Prototype
---
summary: Early implementations used to test workflows, boundaries, and ideas.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: Release
---
summary: Preparing, packaging, publishing, and documenting software releases.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: User Control
---
summary: Explicit user authority, review, consent, and reversible operations.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: v0.4.0
---
summary: Work associated with the version 0.4.0 contract and milestone.
+5
View File
@@ -0,0 +1,5 @@
_model: tag
---
title: Verification
---
summary: Tests, assertions, conformance checks, and reproducible evidence.
@@ -0,0 +1,5 @@
_model: tag
---
title: Writing Workspace
---
summary: Structured environments for drafting, revision, and creative work.
+12
View File
@@ -32,6 +32,18 @@ type = strings
label = Imported topics
type = strings
[fields.topic_labels]
label = Topic display labels
type = strings
[fields.topic_tag_slugs]
label = Approved topic tags
type = strings
[fields.kicker]
label = Type
type = string
[fields.source_commit]
label = Source commit
type = string
+13 -1
View File
@@ -647,12 +647,24 @@ def materialize(
if record.get("_model") == "devlog-entry":
entry_commit = record["source_commit"]
entry_meta = commit_metadata(Path(metadata["mirror_path"]), entry_commit)
topic_values = split_list(record["tags"])
topic_tag_slugs = approved_tag_slugs(
source["project_id"],
"devlog topic",
topic_values,
tag_vocabulary,
)
output = append_fields(
data,
{
"kicker": "Devlog",
"source_commit_url": f"{source['web_url']}/commit/{entry_commit}",
"source_commit_time": entry_meta["commit_date"],
"topic_tags": split_list(record["tags"]),
"topic_tags": topic_values,
"topic_labels": taxonomy_label_items(
topic_values, topic_tag_slugs
),
"topic_tag_slugs": topic_tag_slugs,
},
)
target.write_bytes(output)
+2 -1
View File
@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% from "macros/tag-links.html" import taxonomy_labels with context %}
{% block body %}
{% set project = this.parent.parent %}
{% set siblings = this.get_siblings() %}
@@ -19,7 +20,7 @@
<dt>Source commit</dt>
<dd><a href="{{ this.source_commit_url }}"><code>{{ this.source_commit[:10] }}</code></a></dd>
</div>
{% if this.topic_tags %}<div class="post-info-row"><dt>Topics</dt><dd class="technology-list">{% for tag in this.topic_tags %}<span>{{ tag }}</span>{% endfor %}</dd></div>{% endif %}
{% if this.topic_labels %}<div class="post-info-row"><dt>Topics</dt><dd class="technology-list">{{ taxonomy_labels(this.topic_labels) }}</dd></div>{% endif %}
</dl>
</aside>
+2 -1
View File
@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% from "macros/tag-links.html" import taxonomy_labels with context %}
{% block body %}
{% set project = this.parent %}
<section class="hero compact devlog-hero">
@@ -17,7 +18,7 @@
<div class="entry-meta"><span class="label">Devlog</span><time datetime="{{ entry.date }}">{{ entry.date|dateformat('YYYY.MM.dd') }}</time></div>
<h2><a href="{{ entry|url }}">{{ entry.title }}</a></h2>
<p>{{ entry.summary }}</p>
{% if entry.topic_tags %}<div class="technology-list">{% for tag in entry.topic_tags %}<span>{{ tag }}</span>{% endfor %}</div>{% endif %}
{% if entry.topic_labels %}<div class="technology-list">{{ taxonomy_labels(entry.topic_labels) }}</div>{% endif %}
<a class="read-more" href="{{ entry|url }}">Read entry →</a>
</article>
{% endfor %}
+10 -1
View File
@@ -19,6 +19,16 @@
{% if entries.append(item) %}{% endif %}
{% endfor %}
{% endfor %}
{% for project in site.query('/projects') %}
{% if project._model == 'project' %}
{% set devlog = site.get(project.path ~ '/devlog') %}
{% if devlog %}
{% for entry in devlog.children %}
{% if entries.append(entry) %}{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
{% for item in entries|sort(attribute='date', reverse=true) %}
<article class="timeline-entry accent-{{ item.kicker|lower }}">
<div class="entry-meta">
@@ -33,4 +43,3 @@
</div>
</div>
</section>
+10
View File
@@ -18,6 +18,16 @@
{% if tag._id in item_tags %}{% set usage.count = usage.count + 1 %}{% endif %}
{% endfor %}
{% endfor %}
{% for project in site.query('/projects') %}
{% if project._model == 'project' %}
{% set devlog = site.get(project.path ~ '/devlog') %}
{% if devlog %}
{% for entry in devlog.children %}
{% if tag._id in entry.topic_tag_slugs %}{% set usage.count = usage.count + 1 %}{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
<a class="tag-directory-card" href="{{ tag|url }}">
<span class="tag-directory-name">{{ tag.title }}</span>
<span class="tag-directory-count">{{ usage.count }} {% if usage.count == 1 %}entry{% else %}entries{% endif %}</span>
+12 -2
View File
@@ -8,6 +8,16 @@
{% if this._id in item_tags %}{% set matches.items = matches.items + [item] %}{% endif %}
{% endfor %}
{% endfor %}
{% for project in site.query('/projects') %}
{% if project._model == 'project' %}
{% set devlog = site.get(project.path ~ '/devlog') %}
{% if devlog %}
{% for entry in devlog.children %}
{% if this._id in entry.topic_tag_slugs %}{% set matches.items = matches.items + [entry] %}{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
<section class="hero compact">
<div class="shell narrow">
<p class="eyebrow">Tag</p>
@@ -24,7 +34,7 @@
{% if matches.items %}
<div class="card-grid">
{% for item in matches.items|sort(attribute='date', reverse=true) %}
{% set item_tags = item.taxonomy_tags if item._model == 'project' else item.tags %}
{% set item_tags = item.taxonomy_tags if item._model == 'project' else (item.topic_tag_slugs if item._model == 'devlog-entry' else item.tags) %}
<article class="entry-card accent-{{ item.kicker|lower }}">
<div class="entry-meta"><span class="label">{{ item.kicker }}</span><time>{{ item.date|dateformat('YYYY.MM.dd') }}</time></div>
<h2><a href="{{ item|url }}">{{ item.title }}</a></h2>
@@ -35,7 +45,7 @@
{% endfor %}
</div>
{% else %}
<p class="empty-state">No projects, blog entries, or articles use this tag yet.</p>
<p class="empty-state">No projects, devlog entries, blog entries, or articles use this tag yet.</p>
{% endif %}
</div>
</section>
+5
View File
@@ -15,6 +15,7 @@ from project_sources import ( # noqa: E402
append_fields,
fetch_json,
metadata_digest,
normalize_tag_value,
parse_record,
split_list,
taxonomy_label_items,
@@ -68,6 +69,10 @@ class ProjectSourceTests(unittest.TestCase):
)
self.assertEqual(items, ["typescript\tTypeScript", "-\tUnlisted Tool"])
def test_devlog_topic_versions_get_stable_tag_slugs(self) -> None:
self.assertEqual(normalize_tag_value("v0.4.0"), "v0-4-0")
self.assertEqual(normalize_tag_value("OpenAI Build Week"), "openai-build-week")
if __name__ == "__main__":
unittest.main()