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
86 lines
3.8 KiB
Markdown
86 lines
3.8 KiB
Markdown
# 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, default `http://host.docker.internal:8080`, no trailing
|
|
slash) — where nginx sends `/api/*`. Set it in `.env` or the environment; no
|
|
rebuild needed. On Linux, a host firewall (firewalld, NixOS) may block
|
|
containers from reaching the host — allow the Docker bridges, or point
|
|
`API_URL` at the API directly.
|
|
- `API_URL` as a compile-time define (default `/api`) — the base URL baked into
|
|
`main.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](https://devai.io) — the blog frontend series, one API and four
|
|
clients: [`blog-react`](https://git.devai.io/templates/blog-react),
|
|
[`blog-angular`](https://git.devai.io/templates/blog-angular), `blog-dart`,
|
|
[`blog-flutter`](https://git.devai.io/templates/blog-flutter).
|