The public website of OpenTodo, the open-source task manager you host yourself. Live at https://open-todo.app; the demo runs at https://demo.open-todo.app.
The site is built with Astro to static files and served by nginx. Everything it says about the app (version, docs, screenshots, colours, icons, changelog, import formats) comes from the app's site bundle of a pinned release, never from files copied by hand. How the site is built and what it must do is specified in openspec/ (see AGENTS.md).
Needs Node 22.
npm ci
npm run dev # fetches the pinned bundle into .bundle/, then http://localhost:4321
npm run build # fetches the bundle, checks the content, builds dist/
npm run serve # serves dist/ like nginx on http://127.0.0.1:4321bundle.json pins where app facts come from. Exactly one of:
| Pin | Meaning |
|---|---|
{ "version": "v0.5.0" } |
Today. The release asset opentodo-site-bundle-v0.5.0.tar.gz from the app's GitHub releases, checked against the release's SHA256SUMS. |
{ "preview": "<run id>" } |
The bundle artefact of an app CI run on main (needs GITHUB_TOKEN). Every page shows a "Preview" banner, and production builds refuse it. |
BUNDLE_DIR=/path/to/bundle npm run build uses a local bundle folder instead; that is always a preview.
Updating the site to a new release is a one-line change to bundle.json: pin the new tag, rebuild, and fix any content the new docs or manifest no longer back.
A bundle holds the docs site (generated by the app's make docs), the screenshots, tokens/tokens.css and the icon sprite (icons/icons.svg, with names.json and the Lucide licence), a changelog.json parsed from the app's CHANGELOG.md, and a manifest.json with the version, release date, import formats and the facts the pages may show. Its README.md describes the layout.
| Command | What it fails on |
|---|---|
npm run build |
Also writes sitemap-index.xml and one social preview card per page (dist/og/, from scripts/og.mjs, with the Inter files in scripts/og/fonts/). Fails on: a page that does not render; content front matter that breaks its schema (a comparison without checkedOn or older than 180 days, an OpenTodo claim without a docs link, a guide or template not reviewed in a year); a paragraph that claims a feature without a docs link (scripts/check-content.mjs). |
npm run check |
TypeScript and Astro errors. |
npm test |
Unit tests of the bundle scripts, template generator, content rules and checks. |
npm run check:dist |
Broken internal or docs links and anchors; emoji or symbols used as icons; scripts from origins other than the site and the self-hosted analytics. |
npm run test:browser |
Serious or critical axe violations in light and dark at 360 and 1280 px; horizontal scroll; pages that do not read and navigate without JavaScript; broken islands. Visits one page of every template in src/pages/ and every home page; npm run test:browser:full visits every page (BROWSER_PAGES=all, or list with BROWSER_PAGES_FILE). Needs Playwright's Chromium (npx playwright install chromium). |
npm run templates:check |
A template that does not import cleanly into a server of the pinned release (OPENTODO_URL); also captures each template's screenshots into src/assets/template-shots/. |
GitHub Actions runs them on every pull request and on main (.github/workflows/). The browser checks visit every page on main, nightly (a red night opens a ci-nightly issue) and on a manual run; a pull request visits the template set, or, when it only changes Markdown and images under src/content/, the pages whose HTML differs from the last build of main. The Docker image builds on a pull request only when what goes into it changes (ci-fast-checks).
| Folder | Pages |
|---|---|
src/content/templates/ |
/templates/<slug>: a template tree, turned into a downloadable opentodo-export file at build time |
src/content/guides/, toolkit/ |
Guides and setup toolkits |
src/content/blog/<slug>/index.md |
/blog/<slug>/: news, releases, development and ownership posts (/essays/ redirects here) |
bundle changelog.json |
/whats-new/<version> and /feed.xml |
Content is drafted following content/VOICE.md, picked from content/BACKLOG.md, and arrives through a pull request with a preview; posts stay drafts until the maintainer publishes them.
-
Create a folder
src/content/blog/<slug>/with anindex.md. The folder name is the URL (/blog/<slug>/);page,categoryandfeed.xmlare reserved. Put the post's images in the same folder. -
Start it with front matter:
--- title: "OpenTodo 0.3.0: bring your own AI" summary: One or two sentences for the cards, the feeds and search results (at most 300 characters). publishedAt: 2026-10-14 # the post is hidden until this date updatedAt: 2026-11-02 # optional, shown next to the publish date author: raffaele # a key in src/content/authors.json category: releases # news | releases | development | ownership release: 0.3.0 # releases only, and required there: a version in the pinned bundle's changelog cover: ./cover.jpg # optional; also the social preview image coverAlt: What the cover shows # required with a cover inDevelopment: false # true for work that is not in a release yet shippedIn: 0.4.0 # optional, once an in-development post's work is released draft: true # drafts never render; remove it once the maintainer approves ---
-
Write in Markdown following the "Blog posts" section of
content/VOICE.md. Every claim about what OpenTodo does links to its docs page (/docs/...); a paragraph that only looks like a claim gets<!-- no-claim -->on the line above. Posts withinDevelopment: trueskip the claim rule and show an "In development" notice instead, but their docs links must still resolve. -
Run
node scripts/check-content.mjs(it also runs before everynpm run build) and open a draft pull request. The post appears on the blog index, its category page, the home page,/feed.xmland/blog/feed.xmlonce it is published and the site is built.
- + New Resource → Public Repository,
https://github.com/OpenTodoApp/OpenTodoWebsite, branchmain. - Build pack: Dockerfile. Base directory:
/. Port:80. No environment variables. - Assign the domain
open-todo.appand deploy.
The Dockerfile builds the site (fetching the pinned bundle) and copies dist/ into nginx:1.27-alpine. Any static host works too: publish dist/, serving index.html for directory URLs and 404.html for unknown paths.
Code: MIT (LICENSE). Written content and images: CC BY 4.0 (LICENSE-CONTENT.md). The documentation and screenshots in the bundle follow the app's AGPL-3.0 licence. Inter is under the SIL Open Font License (public/fonts/OFL-Inter.txt), the icons under the ISC licence (icons/LICENSE-lucide.txt in the bundle).