This monorepo contains:
apps/web
: Nuxt 3 SSR blog UIapps/api
: Express API with file-backed storage (data/
uploads)
Use the production Docker setup below to deploy quickly with Docker Hub images and Nginx.
- Use feature branches and conventional commits.
- Before commit/PR:
- Run tests and guards:
npm run test:all
- Ensure hidden-layer content (docs/, internal/, glyphs/) is not staged.
- Run tests and guards:
- Open PRs using the provided template; include governance adherence and runtime ports.
- Merge the feature branch to
main
when all CI checks are green. - Append the latest changes to the
Changelog
section in thisREADME.md
. - Create and push an annotated tag for the release:
VERSION=v0.2.3 git tag -a "$VERSION" -m "release: $VERSION" git push origin "$VERSION"
- Delete the merged branch locally and on origin:
BR=feat/v0.2.3-seo-security-editor git branch -d "$BR" || true git push origin --delete "$BR" || true
- Deploy production images (Docker Hub pull + compose up):
# Ensure .env.prod is present (see .env.prod.example) docker compose --env-file .env.prod -f docker-compose.prod.yml pull docker compose --env-file .env.prod -f docker-compose.prod.yml up -d
- Post-merge smoke test: confirm
.github/workflows/post-merge-smoke.yml
passes against the remote API URL (checks auth, basic endpoints, and og:image). - Governance upkeep: keep branch protection required checks in sync with current workflow job names to avoid stale/phantom failures.
Create a local git hook to block commits if checks fail:
mkdir -p .githooks
cat > .githooks/pre-commit <<'SH'
#!/usr/bin/env bash
set -euo pipefail
npm run test:all
SH
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks
This repository avoids committing private governance/glyph content; CI enforces guards.
Deploy the latest release using prebuilt images from Docker Hub namespace ava2016
.
- Create
.env.prod
next todocker-compose.prod.yml
(use.env.prod.example
as a base):
# DB
POSTGRES_USER=blog
POSTGRES_PASSWORD=change_me
POSTGRES_DB=blogdb
POSTGRES_PORT=5433
# API
JWT_SECRET=super_secret_value
CORS_ORIGIN=https://your.site
PUBLIC_BASE_URL=https://api.your.site
# Web (browser calls API here)
NUXT_PUBLIC_API_BASE=https://api.your.site
- Pull and start services (from repo root):
docker compose --env-file .env.prod -f docker-compose.prod.yml pull
docker compose --env-file .env.prod -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml ps
Notes
- Web runs in the container on PORT=5000; expose via Nginx or port mapping (e.g., 5588:5000).
- API runs on 3000; expose via Nginx or mapping (e.g., 3388:3000).
- Current web image includes the Nuxt vite-builder runtime fix and stable Nitro start.
Rollback
# If you tag images per release, set TAG (e.g., v0.2.4) in .env.prod or compose file
docker compose --env-file .env.prod -f docker-compose.prod.yml pull
docker compose --env-file .env.prod -f docker-compose.prod.yml up -d
We provide a production compose file: docker-compose.prod.yml
.
Replace DOCKER_NS
with your Docker Hub namespace.
# Web (Nuxt SSR)
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t DOCKER_NS/glyph-web:vX.Y.Z \
-t DOCKER_NS/glyph-web:latest \
-f apps/web/Dockerfile apps/web --push
# API (Express)
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t DOCKER_NS/glyph-api:vX.Y.Z \
-t DOCKER_NS/glyph-api:latest \
-f apps/api/Dockerfile apps/api --push
- Ensure Docker + docker compose v2, and Nginx as reverse proxy.
- DNS:
- Web:
yourdomain.com
-> server IP - API:
api.yourdomain.com
-> server IP
- Web:
Create .env.prod
next to docker-compose.prod.yml
(see .env.prod.example
):
DOCKER_NS=your-dockerhub-user
TAG=vX.Y.Z
NUXT_PUBLIC_API_BASE=https://api.yourdomain.com
CORS_ORIGIN=https://yourdomain.com
PUBLIC_BASE_URL=https://api.yourdomain.com
JWT_SECRET=change-me
docker compose --env-file .env.prod -f docker-compose.prod.yml pull
docker compose --env-file .env.prod -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml ps
The API persists uploads in the named volume api_data
mounted at /app/data
.
Terminate TLS at Nginx and proxy to the internal ports:
- Web:
https://yourdomain.com
->http://127.0.0.1:5000
- API:
https://api.yourdomain.com
->http://127.0.0.1:3000
Minimal server blocks:
server { listen 80; server_name yourdomain.com; return 301 https://$host$request_uri; }
server { listen 80; server_name api.yourdomain.com; return 301 https://$host$request_uri; }
server {
listen 443 ssl; server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://127.0.0.1:5000; }
}
server {
listen 443 ssl; server_name api.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
client_max_body_size 20m;
location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://127.0.0.1:3000; }
location /uploads/ { proxy_pass http://127.0.0.1:3000; expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; }
}
Set NUXT_PUBLIC_API_BASE
and PUBLIC_BASE_URL
to the public API domain so og:image
uses absolute URLs. Validate a post URL with Twitter Card Validator after deployment.
# From repo root
npm run test:all
# Web dev
cd apps/web && npm i && npm run dev
# API dev
cd ../api && npm i && npm run dev
The E2E tests run the Nuxt app locally and target the API running in Docker.
- The frontend server runs on
FRONTEND_PORT
(default5999
). - The tests and the Nuxt app talk to the Docker-exposed API via
API_BASE
(defaulthttp://localhost:3388
). - Real JWT tokens are generated in tests using
JWT_SECRET
(HS256).
Add these to your .env
(see .env.example
):
JWT_SECRET=change_me_in_local_env
API_BASE=http://localhost:3388
FRONTEND_PORT=5999
# Optional: used by Nuxt runtime as well
NUXT_PUBLIC_API_BASE=http://localhost:3388
# 1) Start API + DB (in another terminal)
docker compose up -d db api
# 2) From repo root, run Playwright tests
cd apps/web
npm i
npx playwright install --with-deps
npm run test:e2e
Notes:
- Tests will skip if
JWT_SECRET
is missing. - The Playwright config loads env from the repo root
.env
and starts only Nuxt. - If port 5999 is busy, set
FRONTEND_PORT
to another free port in.env
.
.github/workflows/ci.yml
defines ane2e
job that:- Brings up
db
+api
viadocker compose
(API exposed on host:3388
). - Seeds a CI
.env
withJWT_SECRET
(fromE2E_JWT_SECRET
secret),API_BASE
, andNUXT_PUBLIC_API_BASE
. - Installs Playwright browsers and runs the E2E suite with the frontend on
FRONTEND_PORT=5999
. - Tears down Docker after completion.
- Brings up
- API: add file-backed
settings.json
with{ defaultTheme, blogName }
; exposeGET /api/settings
andPUT /api/settings
(auth) and ensure settings file exists inensureStore()
. - Web (theme & persistence):
- Persist admin-selected default theme via
PUT /api/settings
from header toggle. - Load server default theme for guests when no local preference.
- Persist blog name on title save; load blog name for guests when no local override.
- Persist admin-selected default theme via
- Web (UI):
- Modal theme-aware styling (panel/border/buttons/textarea using theme variables).
- Toasts readable on light theme; keep translucent look on dark theme.
- Post list: client fallback fetch when SSR misses, then
nextTick()+setupReveal()
so cards render visibly; long words/URLs wrap safely. - New-post form, inputs, and buttons now theme-aware; card background visuals reduced to ~20% opacity.
- Governance: centralized glyph mesh updated (removed
GOV-SEC-001
, addedGVN-006
,GVN-008
).
- API: RFC 6750-compliant auth errors (WWW-Authenticate) and specific codes (token_missing, token_invalid, token_expired, insufficient_scope, auth_required).
- Web: surface API auth errors via toast with friendly messages; footer shows app version via NUXT_PUBLIC_APP_VERSION.
- Docs: Quick Deploy guide for Docker Hub namespace
ava2016
. - Governance: GOV-CORE-013 Post-Merge Ritual formalized; workflow auto-deletes merged branches and posts changelog reminder.
- CI/Vitest: remove unsupported CLI flags; scope via
apps/web/vitest.config.ts
test.include
. - CI: run
web-build
insideapps/web
; add diagnostics (commit SHA, scripts, Vitest version). - E2E: Dockerized API healthchecks and health wait loop for stable Playwright runs.
- Workflows:
Glyph & Docs Guard
installsapps/api
deps and adds diagnostics; roottest:all
installs API deps. - Post-merge smoke workflow added to validate prod-like API URL.
- Docs: add Sprint End Ritual and changelog.
- API: RFC 6750-compliant auth errors (WWW-Authenticate) with specific codes (
token_missing
,token_invalid
,token_expired
,insufficient_scope
,auth_required
). - Web: surface API auth errors via toast with friendly messages; add footer version label (
Blog vX.Y.Z
).