Skip to content

feat(blog): an optional, owner-configured collections page replaces the hard-wired research page - #484

Merged
kh0pper merged 10 commits into
mainfrom
feat/blog-collections
Oct 15, 2026
Merged

kh0pper merged 10 commits into
mainfrom
feat/blog-collections

Conversation

@kh0pper

@kh0pper kh0pper commented Oct 15, 2026

Copy link
Copy Markdown
Owner

/blog/research used to be wired to one specific project: its id, titles and blurbs were in the code. It is now an optional collections page driven by an owner-written ~/.crow/blog/collections.json. With no file, the page is off and its nav link is hidden.

Server

  • loadCollectionsConfig validates the file with zod and caches it by mtime.
    • A missing file means the page is off. An invalid, unreadable or deleted file disables it with a single warning per mtime. The warning logs only the path and issue codes, never values.
    • The number of sections is capped, and projectId must be a positive integer, passed as a bound parameter.
    • docLinks.base must be a relative /blog/ path or an http(s) URL.
  • Owner blurbs pass through a limited sanitizer that keeps only em, strong and http(s) links. Nothing in the code writes the config file, and a test enforces that.
  • The research page, its nav link and the chapter nav all come from the config. The configured language sets <html lang> and the locale.
  • The four source-archive routes (/blog/sources/*) leave the public router. They currently answer 500 on hosts without their table and are now 404. The general Funnel invariant test ("/blog stays in PUBLIC_FUNNEL_PREFIXES") moves to tests/auth-network.test.js.
  • The figures bucket (CROW_BLOG_FIGURES_BUCKET, default blog-figures), the internal gateway URL (defaults to this gateway's own port) and the hydrate-asset folder become instance settings.
  • data-dashboard 1.1.1 declares CROW_BLOG_FIGURES_BUCKET, with no default so it can't override the gateway's value.
  • A case-study post renders normally when the data-dashboard tables are absent.

Tests: full suite 8815 run, 8814 pass, 0 fail, 1 skipped (the opt-in backup rehearsal). Every new behaviour was test-first. Public-hygiene is clean.

Deploy note: an instance that wants the page writes its own config, and setting the figures bucket is optional. A gateway restart is needed after merge.

https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

kh0pper added 10 commits October 15, 2026 11:27
A strict, read-only loader for <CROW_HOME>/blog/collections.json
(CROW_BLOG_COLLECTIONS_CONFIG overrides). A missing file is off and silent;
an unreadable, malformed or out-of-schema file is off, never throws, and
warns once per (mtime, code) with the path and issue codes only.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
em, strong and a[href] with an absolute http(s) URL only; relative,
protocol-relative and non-http hrefs are dropped and kept links get
rel="noopener noreferrer". Built on the existing sanitize-html dependency.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
…m the collections config

With no config the page is a 404, the nav carries no link to it and posts
get no chapter nav. With one, the page renders configured case-study and
tag sections (project id bound as a parameter, LIMIT 200, escaped strings,
sanitized featured blurb), pageShell takes a lang option, and the chapter
nav is scoped to the configured project. A host without the data-dashboard
table renders the empty state instead of an error. New en/es strings under
blog.collections.*.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
/blog/sources/* served one owner's private archive from a table the core
schema does not create, so on most hosts it returned 500. The routes, their
per-route Funnel block and their test go; they now 404. The general Funnel
invariant that test carried (/blog stays a public Funnel prefix) moves into
tests/auth-network.test.js.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
… become instance settings

CROW_BLOG_FIGURES_BUCKET (default blog-figures) replaces the hard-coded
bucket in the embed API and the data-dashboard server, read per call. The
internal gateway URL defaults to this gateway's own listen port instead of a
fixed :3002 (BLOG_FIGURE_GATEWAY_URL still overrides). Hydrate assets come
from CROW_BLOG_HYDRATE_ASSETS_DIR, else <CROW_HOME>/bundles/tea-maps, with
no home-directory fallbacks. data-dashboard 1.1.1 declares the bucket var.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
servers/ and bundles/ may not name the old private tables, a fixed project
id, private bucket names or a personal checkout path, and only the read-only
loader may name the blog collections config. Rewords the tea-maps mount
comment.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
Declaring CROW_BLOG_FIGURES_BUCKET in the bundle's env_vars let a value set
in the Extensions UI win inside the bundle child while the gateway's
/blog/figures route kept reading its own env: uploads and reads could hit
different buckets. The bundle inherits the gateway env, so the declaration
goes (1.1.1 stays). The guide says to set it in the gateway's .env or a
systemd drop-in, and its docLinks example uses an https base, noting that a
/blog/ base needs something to serve that path.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
%2e, %2f and %5c (either case) are refused alongside literal .., // and
backslashes.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
…error text

A 500 from /blog/api/chart/:id/data.json sent err.message (SQL errors,
table names) to anonymous Funnel callers. The response is now just
{error:"internal_error"}; the message stays in the server log.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
@kh0pper
kh0pper merged commit 76b58a5 into main Oct 15, 2026
4 checks passed
@kh0pper
kh0pper deleted the feat/blog-collections branch October 15, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant