4.6 KiB
SoftVowels — project conventions
A monorepo with two apps:
apps/web Astro 5 (static site, Tailwind v4)
apps/cms Payload 3 (admin UI + REST/GraphQL, Next.js 15)
Source paths
apps/web/src/components/{layout,home,article,category,search,newsletter,ui}/— componentsapps/web/src/layouts/— page layoutsapps/web/src/lib/— Payload client, types, SEO, formattingapps/web/src/styles/—global.css(Tailwind v4@themetokens) andprose.cssapps/web/public/fonts/— dropGeist-Variable.woff2+GeistMono-Variable.woff2hereapps/cms/src/collections/— Payload collectionsapps/cms/src/hooks/— Payload hooksapps/cms/src/lib/— Payload helpers (e.g.withRevalidate)
Code style
- All component files are
.astrosingle-file components. Frontmatter (between---) is TypeScript. - No comments in code unless explicitly requested.
- Tailwind v4 tokens live in CSS (
@themeinglobal.css). Do not create atailwind.config.*file. - Use
icon(astro-icon) with names likematerial-symbols:memory. Self-host icon font woff2 underpublic/icons/material-symbols-outlined.woff2and wire it via@font-faceinglobal.css(already declared; add the binary file when ready). - Prefer
border border-border-subtle rounded-mdover arbitrary values to stay on the 4px baseline. - For code blocks in MDX / Lexical output, the article body uses
prose-article(seeprose.css).
Lint / typecheck (web app)
cd apps/web
npx astro check
npx prettier --check "src/**/*.{ts,astro,css}"
There is no separate ESLint run; the project's quality bar is enforced by astro check and Prettier.
Local development (optional, not required for homeserver deploy)
If you have Node + pnpm on your dev machine:
pnpm install
pnpm dev:web # Astro on :4321
pnpm dev:cms # Payload on :3000
The homeserver deploy does not require Node or pnpm on the host — Docker builds both images from source.
Build outputs
apps/web/dist/is generated byastro buildinside thesoftvowels-webDocker image. Baked into the image at/usr/share/nginx/html/.apps/cms/.next/standalone/is generated bynext build(withoutput: 'standalone') inside thesoftvowels-cmsDocker image. Baked into the image and run vianode server.js.apps/cms/media/(uploaded files) lives in a named Docker volume (softvowels-cms-uploads), not the image.
Deploy (homeserver, Docker only)
The host needs Docker and Docker Compose v2. Nothing else.
# 1. Get the code on the server
git clone <repo> /opt/softvowels
cd /opt/softvowels
# 2. Configure environment
cp .env.example .env
$EDITOR .env # fill in POSTGRES_PASSWORD, PAYLOAD_SECRET, hostnames
# 3. Build and start
docker compose up -d --build
# 4. Watch the first boot
docker compose logs -f softvowels-cms
# On first run, Payload prints a one-time URL for creating the admin
# account. Open it, then visit https://cms.softvowels.example/admin.
Updating the site (no webhook yet)
Until the webhook is in place, each site update requires:
cd /opt/softvowels
git pull
docker compose build softvowels-web
docker compose up -d softvowels-web
That's the entire "redeploy" for a content change. The CMS does not need rebuilding for content changes — only the web image.
Nginx Proxy Manager setup
Add two proxied hosts in NPM:
| Domain | Forward to | Notes |
|---|---|---|
softvowels.example |
softvowels-web:80 (http) |
Force SSL, HSTS, HTTP/2, websockets off |
cms.softvowels.example |
softvowels-cms:3000 (http) |
Force SSL, HSTS, websockets on (Payload admin uses them) |
NPM must be able to resolve softvowels-web and softvowels-cms. The nginx_proxy_manager network is external: true in docker-compose.yml; the easiest way to make NPM see the containers is to run NPM on the same host and add the network to its container with:
docker network connect nginx_proxy_manager <npm-container-name>
Self-hosting fonts
Drop these two files into apps/web/public/fonts/ (before docker compose build):
Geist-Variable.woff2(https://github.com/vercel/geist-font — renameGeistVF.woff2)GeistMono-Variable.woff2(renameGeistMonoVF.woff2)
The @font-face rules are already declared in global.css.
Environment
Root .env (used by docker compose):
POSTGRES_PASSWORD=
PAYLOAD_SECRET=
WEB_HOSTNAME= # e.g. softvowels.example
CMS_HOSTNAME= # e.g. cms.softvowels.example
REVALIDATE_TOKEN was removed when the webhook was deferred. Re-add it to .env and docker-compose.yml when the on-publish rebuild is wired back in.