- Dart 75%
- CSS 20.3%
- Dockerfile 3%
- HTML 1.7%
Pinned 2026 toolchains (Go 1.26, Rust 1.98/edition 2024, Python 3.14 + uv, Node 24, Zig 0.16, NixOS 26.05), postgres 18 / mongo 8, lockfiles built from, non-root runtimes, .dockerignore, per-project LICENSE, READMEs with the git.devai.io clone line, checkout@v7 CI. Security fixes in the legacy Rust APIs (any-password login, self-assigned admin, hard-coded JWT secret), JWT alg/exp/sub enforcement across the blog series, safe markdown links in the frontends, and many smaller bugs — every project was built, run and exercised end to end. Adds scripts/publish.sh + a CI publish job that splits every folder into its own repo at git.devai.io/templates/<folder>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01128fhuZbgivaSJvtMf4s1G |
||
|---|---|---|
| .github/workflows | ||
| web | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| analysis_options.yaml | ||
| compose.yaml | ||
| Dockerfile | ||
| LICENSE | ||
| nginx.conf.template | ||
| pubspec.lock | ||
| pubspec.yaml | ||
| README.md | ||
blog-dart
The blog client as a hand-rolled Dart web SPA — no Flutter, no framework, no CSS
library: package:web for the DOM, a hash router, one stylesheet, and dart compile js.
Post list, post page with rendered markdown, login, and an editor with a publish
toggle. It works against any backend of the devai.io blog engine series.
Run
Get it: git clone https://git.devai.io/templates/blog-dart.git
docker compose up --build
Open http://localhost:8081. The app expects a blog API on http://localhost:8080 —
start one of the backend siblings first (e.g. docker compose up --build in
blog-go-postgres), then create a user, since there is no sign-up page:
curl -X POST localhost:8080/auth/register -H 'Content-Type: application/json' \
-d '{"email":"me@example.com","password":"secret123"}'
Without Docker (Dart 3.13), check and compile:
dart pub get
dart analyze
dart compile js -O2 -o build/main.dart.js web/main.dart
How it works
The browser only ever talks to one origin. The app calls /api/... and nginx
forwards /api/* to API_URL — so the API needs no CORS setup.
API_URL(runtime, defaulthttp://host.docker.internal:8080, no trailing slash) — where nginx sends/api/*. Set it in.envor the environment; no rebuild needed. On Linux, a host firewall (firewalld, NixOS) may block containers from reaching the host — allow the Docker bridges, or pointAPI_URLat the API directly.API_URLas a compile-time define (default/api) — the base URL baked intomain.dart.js:dart compile js -DAPI_URL=https://api.example.com .... Use a full URL only if the app should call an API directly; that API must then send CORS headers.
Everything lives in web/main.dart (~480 lines): a typed fetch wrapper, the
Post/PostSummary models, a small DOM helper, the markdown renderer and four
views dispatched from the hash route (#/, #/posts/<slug>, #/login,
#/write, #/edit/<slug>) — hash routes need no server-side fallback.
Auth: POST /auth/login returns a JWT. It is kept in memory, mirrored to
localStorage (blog_token) so a reload stays signed in, and sent as
Authorization: Bearer <token>. Log out clears both.
Markdown becomes DOM nodes whose text is set with textContent — nothing is
parsed as HTML — and only http(s):, mailto: and relative links become anchors,
so a post body cannot inject markup.
The API never returns unpublished posts, so the editor keeps a freshly saved draft open; tick Published and save again to make it public.
API calls used:
POST /auth/login {email, password} -> {token}
GET /posts -> [{id, title, slug, excerpt, published_at}]
GET /posts/{slug} -> full post
POST /posts (auth) {title, body} -> post (unpublished)
PUT /posts/{id} (auth) {title?, body?, published?} -> post
DELETE /posts/{id} (auth) -> 204
Layout
web/index.html shell page: loads styles.css and main.dart.js
web/main.dart API client, markdown renderer, views, hash router
web/styles.css plain CSS, light and dark via prefers-color-scheme
nginx.conf.template /api proxy; API_URL filled in at start-up
Deploy
Push to your own GitHub repo and the shipped workflow
(.github/workflows/ci.yml) tests the compose stack, publishes the image to
GHCR, and — once you set the DEPLOY_HOST / DEPLOY_USER variables and
DEPLOY_KEY secret — deploys it to your server over ssh. Set API_URL in
/srv/blog-dart/.env on the server to reach your API.
Part of devai.io — the blog frontend series, one API and four
clients: blog-react,
blog-angular, blog-dart,
blog-flutter.