fix: payload building issues
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
# Required
|
# Required
|
||||||
POSTGRES_PASSWORD=change-me-strong-password
|
POSTGRES_PASSWORD=change-me-strong-password
|
||||||
PAYLOAD_SECRET=change-me-to-a-very-long-random-string
|
PAYLOAD_SECRET=change-me-to-a-very-long-random-string
|
||||||
REVALIDATE_TOKEN=change-me-shared-token
|
|
||||||
|
|
||||||
# Hostnames (used to build CORS / canonical URLs)
|
# Hostnames (used to build CORS / canonical URLs)
|
||||||
WEB_HOSTNAME=softvowels.example
|
WEB_HOSTNAME=softvowels.example
|
||||||
|
|||||||
@@ -4,23 +4,7 @@ A monorepo with two apps:
|
|||||||
|
|
||||||
```
|
```
|
||||||
apps/web Astro 5 (static site, Tailwind v4)
|
apps/web Astro 5 (static site, Tailwind v4)
|
||||||
apps/cms Payload 3 (admin UI + REST/GraphQL)
|
apps/cms Payload 3 (admin UI + REST/GraphQL, Next.js 15)
|
||||||
```
|
|
||||||
|
|
||||||
## Package manager
|
|
||||||
|
|
||||||
`pnpm` 9. From the repo root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install # install both
|
|
||||||
pnpm dev # dev:web
|
|
||||||
pnpm dev:web # Astro only
|
|
||||||
pnpm dev:cms # Payload only
|
|
||||||
pnpm build # build:web
|
|
||||||
pnpm build:cms # build Payload
|
|
||||||
pnpm typecheck # Astro check
|
|
||||||
pnpm format # prettier
|
|
||||||
pnpm format:check # prettier --check
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Source paths
|
## Source paths
|
||||||
@@ -31,7 +15,8 @@ pnpm format:check # prettier --check
|
|||||||
- `apps/web/src/styles/` — `global.css` (Tailwind v4 `@theme` tokens) and `prose.css`
|
- `apps/web/src/styles/` — `global.css` (Tailwind v4 `@theme` tokens) and `prose.css`
|
||||||
- `apps/web/public/fonts/` — drop `Geist-Variable.woff2` + `GeistMono-Variable.woff2` here
|
- `apps/web/public/fonts/` — drop `Geist-Variable.woff2` + `GeistMono-Variable.woff2` here
|
||||||
- `apps/cms/src/collections/` — Payload collections
|
- `apps/cms/src/collections/` — Payload collections
|
||||||
- `apps/cms/src/hooks/` — Payload hooks (e.g. `revalidateAstro`)
|
- `apps/cms/src/hooks/` — Payload hooks
|
||||||
|
- `apps/cms/src/lib/` — Payload helpers (e.g. `withRevalidate`)
|
||||||
|
|
||||||
## Code style
|
## Code style
|
||||||
|
|
||||||
@@ -42,46 +27,87 @@ pnpm format:check # prettier --check
|
|||||||
- Prefer `border border-border-subtle rounded-md` over arbitrary values to stay on the 4px baseline.
|
- Prefer `border border-border-subtle rounded-md` over arbitrary values to stay on the 4px baseline.
|
||||||
- For code blocks in MDX / Lexical output, the article body uses `prose-article` (see `prose.css`).
|
- For code blocks in MDX / Lexical output, the article body uses `prose-article` (see `prose.css`).
|
||||||
|
|
||||||
## Lint / typecheck
|
## Lint / typecheck (web app)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm typecheck # astro check
|
cd apps/web
|
||||||
pnpm format:check
|
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.
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
## Build outputs
|
||||||
|
|
||||||
- `apps/web/dist/` is bind-mounted into the `web` nginx container (`./apps/web/dist:/usr/share/nginx/html:ro`). The host-side webhook rewrites this folder on demand; the nginx container does not need to be rebuilt.
|
- `apps/web/dist/` is generated by `astro build` inside the `softvowels-web` Docker image. Baked into the image at `/usr/share/nginx/html/`.
|
||||||
- `apps/cms/dist/` is built by Payload (Next.js) and runs as the CMS Node process.
|
- `apps/cms/.next/standalone/` is generated by `next build` (with `output: 'standalone'`) inside the `softvowels-cms` Docker image. Baked into the image and run via `node server.js`.
|
||||||
|
- `apps/cms/media/` (uploaded files) lives in a named Docker volume (`softvowels-cms-uploads`), not the image.
|
||||||
|
|
||||||
## Deploy (homeserver)
|
## Deploy (homeserver, Docker only)
|
||||||
|
|
||||||
1. Copy the repo to `/opt/softvowels` on the server.
|
The host needs Docker and Docker Compose v2. Nothing else.
|
||||||
2. `cp .env.example .env` and fill in the secrets.
|
|
||||||
3. `docker compose up -d --build`.
|
|
||||||
4. In Nginx Proxy Manager add two proxied hosts:
|
|
||||||
- `softvowels.example` → `web:80` (force SSL, HSTS)
|
|
||||||
- `cms.softvowels.example` → `cms:3000` (force SSL, HSTS)
|
|
||||||
5. On the host, install the rebuild webhook:
|
|
||||||
```bash
|
|
||||||
sudo cp scripts/softvowels-webhook.service /etc/systemd/system/
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable --now softvowels-webhook
|
|
||||||
```
|
|
||||||
Make sure `REBUILD_TOKEN` in `/etc/systemd/system/softvowels-webhook.service` matches `REVALIDATE_TOKEN` in `.env`.
|
|
||||||
|
|
||||||
## Revalidate flow
|
```bash
|
||||||
|
# 1. Get the code on the server
|
||||||
|
git clone <repo> /opt/softvowels
|
||||||
|
cd /opt/softvowels
|
||||||
|
|
||||||
1. Editor publishes in `/admin`.
|
# 2. Configure environment
|
||||||
2. Payload `afterChange` hook POSTs to `http://host.docker.internal:4321/__revalidate?secret=…&collection=…&op=…`.
|
cp .env.example .env
|
||||||
3. The host-side Node webhook spawns `scripts/rebuild.sh`, which `git pull`s and runs `pnpm --filter web build`.
|
$EDITOR .env # fill in POSTGRES_PASSWORD, PAYLOAD_SECRET, hostnames
|
||||||
4. nginx container now serves the new `apps/web/dist/`.
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network connect nginx_proxy_manager <npm-container-name>
|
||||||
|
```
|
||||||
|
|
||||||
## Self-hosting fonts
|
## Self-hosting fonts
|
||||||
|
|
||||||
Drop these two files into `apps/web/public/fonts/`:
|
Drop these two files into `apps/web/public/fonts/` (before `docker compose build`):
|
||||||
|
|
||||||
- `Geist-Variable.woff2` (https://github.com/vercel/geist-font — rename `GeistVF.woff2`)
|
- `Geist-Variable.woff2` (https://github.com/vercel/geist-font — rename `GeistVF.woff2`)
|
||||||
- `GeistMono-Variable.woff2` (rename `GeistMonoVF.woff2`)
|
- `GeistMono-Variable.woff2` (rename `GeistMonoVF.woff2`)
|
||||||
@@ -95,28 +121,8 @@ Root `.env` (used by `docker compose`):
|
|||||||
```
|
```
|
||||||
POSTGRES_PASSWORD=
|
POSTGRES_PASSWORD=
|
||||||
PAYLOAD_SECRET=
|
PAYLOAD_SECRET=
|
||||||
REVALIDATE_TOKEN=
|
WEB_HOSTNAME= # e.g. softvowels.example
|
||||||
WEB_HOSTNAME=
|
CMS_HOSTNAME= # e.g. cms.softvowels.example
|
||||||
CMS_HOSTNAME=
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`apps/web/.env`:
|
`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.
|
||||||
|
|
||||||
```
|
|
||||||
PUBLIC_SITE_URL=
|
|
||||||
PUBLIC_PAYLOAD_API=
|
|
||||||
PAYLOAD_API_INTERNAL=
|
|
||||||
REVALIDATE_TOKEN=
|
|
||||||
```
|
|
||||||
|
|
||||||
`apps/cms/.env`:
|
|
||||||
|
|
||||||
```
|
|
||||||
PAYLOAD_SECRET=
|
|
||||||
DATABASE_URI=
|
|
||||||
PAYLOAD_PUBLIC_SERVER_URL=
|
|
||||||
SERVER_URL=
|
|
||||||
CORS_ORIGINS=
|
|
||||||
ASTRO_REVALIDATE_URL=
|
|
||||||
ASTRO_REVALIDATE_TOKEN=
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# SoftVowels
|
# SoftVowels
|
||||||
|
|
||||||
> A premium tech publication. **Astro 5 (static)** frontend + **PayloadCMS 3** headless backend, deployed with **Docker** and fronted by **Nginx Proxy Manager**.
|
> A premium tech publication. **Astro 5 (static)** frontend + **PayloadCMS 3** headless backend, deployed with **Docker only** and fronted by **Nginx Proxy Manager**.
|
||||||
|
|
||||||
SoftVowels is a content-driven site for long-form engineering writing. Editors write in `/admin`; the public site is regenerated on demand and served as static HTML by nginx.
|
SoftVowels is a content-driven site for long-form engineering writing. Editors write in `/admin`; the public site is served as static HTML from a self-contained nginx container.
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
@@ -10,9 +10,8 @@ SoftVowels is a content-driven site for long-form engineering writing. Editors w
|
|||||||
- [Repository layout](#repository-layout)
|
- [Repository layout](#repository-layout)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Quick start (local dev)](#quick-start-local-dev)
|
- [Quick start (local dev)](#quick-start-local-dev)
|
||||||
- [Production build](#production-build)
|
|
||||||
- [Docker deployment (homeserver)](#docker-deployment-homeserver)
|
- [Docker deployment (homeserver)](#docker-deployment-homeserver)
|
||||||
- [On-demand rebuilds (webhook flow)](#on-demand-rebuilds-webhook-flow)
|
- [Updating the site after a content change](#updating-the-site-after-a-content-change)
|
||||||
- [Nginx Proxy Manager setup](#nginx-proxy-manager-setup)
|
- [Nginx Proxy Manager setup](#nginx-proxy-manager-setup)
|
||||||
- [Customising the design](#customising-the-design)
|
- [Customising the design](#customising-the-design)
|
||||||
- [Self-hosting fonts](#self-hosting-fonts)
|
- [Self-hosting fonts](#self-hosting-fonts)
|
||||||
@@ -25,105 +24,84 @@ SoftVowels is a content-driven site for long-form engineering writing. Editors w
|
|||||||
| Layer | Technology |
|
| Layer | Technology |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Frontend | [Astro 5](https://astro.build/) — `output: 'static'`, Tailwind v4, MDX, sitemap, RSS |
|
| Frontend | [Astro 5](https://astro.build/) — `output: 'static'`, Tailwind v4, MDX, sitemap, RSS |
|
||||||
| Backend / CMS | [PayloadCMS 3](https://payloadcms.com/) running on Next.js 15 |
|
| Backend / CMS | [PayloadCMS 3](https://payloadcms.com/) on Next.js 15 (standalone output) |
|
||||||
| Database | PostgreSQL 16 (one schema, one volume) |
|
| Database | PostgreSQL 16 (one schema, one volume) |
|
||||||
| Static host | nginx 1.27 (one container, serves `apps/web/dist/`) |
|
| Static host | nginx 1.27 (one container, dist baked into the image) |
|
||||||
| Reverse proxy / TLS | [Nginx Proxy Manager](https://nginxproxymanager.com/) on the homeserver |
|
| Reverse proxy / TLS | [Nginx Proxy Manager](https://nginxproxymanager.com/) on the homeserver |
|
||||||
| Container runtime | Docker + Docker Compose v2 |
|
| Container runtime | Docker + Docker Compose v2 |
|
||||||
| Language / runtime | Node.js 20+, pnpm 9 |
|
|
||||||
|
**The homeserver only needs Docker.** No Node, no pnpm, no git is required on the host at runtime — every image is built from source by the compose stack.
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── apps/
|
├── apps/
|
||||||
│ ├── web/ # Astro 5 frontend (static)
|
│ ├── web/ # Astro 5 frontend (static)
|
||||||
│ │ ├── src/
|
│ │ ├── src/
|
||||||
│ │ │ ├── components/ # layout, home, article, category, search, newsletter, ui
|
│ │ │ ├── components/ # layout, home, article, category, search, newsletter, ui
|
||||||
│ │ │ ├── layouts/ # BaseLayout, ArticleLayout
|
│ │ │ ├── layouts/ # BaseLayout, ArticleLayout
|
||||||
│ │ │ ├── lib/ # payload client, types, seo, format
|
│ │ │ ├── lib/ # payload client, types, seo, format
|
||||||
│ │ │ ├── pages/ # routes
|
│ │ │ ├── pages/ # routes
|
||||||
│ │ │ └── styles/ # global.css (@theme tokens), prose.css
|
│ │ │ └── styles/ # global.css (@theme tokens), prose.css
|
||||||
│ │ ├── public/fonts/ # drop Geist*.woff2 here
|
│ │ ├── public/fonts/ # drop Geist*.woff2 here
|
||||||
│ │ ├── Dockerfile # build → dist, copy to nginx
|
│ │ ├── Dockerfile # pnpm build -> nginx alpine
|
||||||
│ │ ├── nginx-default.conf # serves dist/, gzip, /admin → CMS
|
│ │ ├── nginx-default.conf # serves dist/, gzip
|
||||||
|
│ │ ├── .dockerignore
|
||||||
│ │ └── astro.config.mjs
|
│ │ └── astro.config.mjs
|
||||||
│ └── cms/ # PayloadCMS 3 admin
|
│ └── cms/ # PayloadCMS 3 admin
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── collections/ # Articles, Authors, Categories, Tags, Editions, Subscribers, Media, Users
|
│ │ ├── collections/ # Articles, Authors, Categories, Tags, Editions, Subscribers, Media, Users
|
||||||
│ │ ├── hooks/ # revalidateAstro (afterChange → POST webhook)
|
│ │ ├── hooks/ # revalidateAstro (webhook, currently unused)
|
||||||
│ │ └── app/ # Next.js routes
|
│ │ ├── lib/ # withRevalidate helper
|
||||||
│ ├── Dockerfile
|
│ │ └── app/ # Next.js routes
|
||||||
|
│ ├── Dockerfile # pnpm build -> Next standalone
|
||||||
|
│ ├── .dockerignore
|
||||||
│ └── payload.config.ts
|
│ └── payload.config.ts
|
||||||
├── scripts/
|
├── scripts/ # (legacy) host-side webhook + rebuild; not used currently
|
||||||
│ ├── rebuild.sh # git pull + pnpm --filter web build
|
├── docker-compose.yml # db + cms + web, all in one
|
||||||
│ ├── webhook.js # host-side Node webhook receiver
|
|
||||||
│ └── softvowels-webhook.service
|
|
||||||
├── docker-compose.yml # db + cms + web
|
|
||||||
├── .env.example
|
├── .env.example
|
||||||
├── AGENTS.md # contributor conventions
|
├── AGENTS.md # contributor conventions
|
||||||
└── README.md # ← you are here
|
└── README.md # ← you are here
|
||||||
```
|
```
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Node.js 20.11+** (`node --version`)
|
|
||||||
- **pnpm 9** (`npm i -g pnpm`)
|
|
||||||
- **Docker 24+** with Compose v2 (`docker compose version`)
|
- **Docker 24+** with Compose v2 (`docker compose version`)
|
||||||
- A homeserver (or any Linux box) with **Nginx Proxy Manager** already running
|
- A homeserver (or any Linux box) with **Nginx Proxy Manager** already running
|
||||||
- Two DNS records pointing at the homeserver:
|
- Two DNS records pointing at the homeserver:
|
||||||
- `softvowels.example` (the public site)
|
- `softvowels.example` (the public site)
|
||||||
- `cms.softvowels.example` (the admin)
|
- `cms.softvowels.example` (the admin)
|
||||||
|
|
||||||
|
That's it. No Node, no pnpm, no git on the host at runtime.
|
||||||
|
|
||||||
## Quick start (local dev)
|
## Quick start (local dev)
|
||||||
|
|
||||||
Local dev doesn't need Docker for the apps themselves; only Postgres is required.
|
Optional — only useful if you want to iterate on the source code on your dev machine. Production deployment is below.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Install workspace dependencies
|
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
# 2. Copy environment templates
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
cp apps/web/.env.example apps/web/.env
|
cp apps/web/.env.example apps/web/.env
|
||||||
cp apps/cms/.env.example apps/cms/.env
|
cp apps/cms/.env.example apps/cms/.env
|
||||||
|
docker compose up -d softvowels-db
|
||||||
# 3. Fill in the secrets in .env (at minimum POSTGRES_PASSWORD and PAYLOAD_SECRET)
|
|
||||||
|
|
||||||
# 4. Start Postgres in Docker
|
|
||||||
docker compose up -d db
|
|
||||||
|
|
||||||
# 5. Run the two apps
|
|
||||||
pnpm dev:cms # Payload on http://localhost:3000/admin
|
pnpm dev:cms # Payload on http://localhost:3000/admin
|
||||||
pnpm dev:web # Astro on http://localhost:4321
|
pnpm dev:web # Astro on http://localhost:4321
|
||||||
```
|
```
|
||||||
|
|
||||||
The Astro dev server fetches articles from `PAYLOAD_API_INTERNAL=http://localhost:3000`. If Payload is empty, every page still renders a graceful empty state.
|
> First Payload boot prints a one-time URL in the logs to create the first admin user. Open it in your browser.
|
||||||
|
|
||||||
> First Payload boot will print a one-time URL in the logs to create the first admin user. Open it in your browser.
|
|
||||||
|
|
||||||
## Production build
|
|
||||||
|
|
||||||
Build both apps as containers:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm build # builds apps/web (static)
|
|
||||||
pnpm build:cms # builds apps/cms (Next.js production server)
|
|
||||||
```
|
|
||||||
|
|
||||||
Or build the containers directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose build
|
|
||||||
```
|
|
||||||
|
|
||||||
The static site is emitted to `apps/web/dist/` and is what the `web` nginx container serves.
|
|
||||||
|
|
||||||
## Docker deployment (homeserver)
|
## Docker deployment (homeserver)
|
||||||
|
|
||||||
The repo ships a `docker-compose.yml` with three services: `db`, `cms`, and `web`.
|
The repo ships a `docker-compose.yml` with three services, all on the external `nginx_proxy_manager` network:
|
||||||
|
|
||||||
### One-time setup on the server
|
| Service | Image | Port (internal) | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `softvowels-db` | `postgres:16-alpine` | 5432 | data store |
|
||||||
|
| `softvowels-cms` | built from `apps/cms/Dockerfile` | 3000 | Payload admin + REST/GraphQL API |
|
||||||
|
| `softvowels-web` | built from `apps/web/Dockerfile` | 80 | static site served by nginx |
|
||||||
|
|
||||||
|
### One-time setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Get the code on the server
|
# 1. Get the code on the server
|
||||||
@@ -141,18 +119,17 @@ $EDITOR .env
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `POSTGRES_PASSWORD` | Postgres password (used by `db` and `cms`) |
|
| `POSTGRES_PASSWORD` | Postgres password (used by `db` and `cms`) |
|
||||||
| `PAYLOAD_SECRET` | Long random string, signs Payload sessions |
|
| `PAYLOAD_SECRET` | Long random string, signs Payload sessions |
|
||||||
| `REVALIDATE_TOKEN` | Shared secret for the `/__revalidate` webhook |
|
|
||||||
| `WEB_HOSTNAME` | e.g. `softvowels.example` |
|
| `WEB_HOSTNAME` | e.g. `softvowels.example` |
|
||||||
| `CMS_HOSTNAME` | e.g. `cms.softvowels.example` |
|
| `CMS_HOSTNAME` | e.g. `cms.softvowels.example` |
|
||||||
|
|
||||||
Generate them with:
|
Generate secrets with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openssl rand -hex 32 # for PAYLOAD_SECRET
|
openssl rand -hex 32 # for PAYLOAD_SECRET
|
||||||
openssl rand -hex 24 # for REVALIDATE_TOKEN
|
openssl rand -hex 24 # for POSTGRES_PASSWORD
|
||||||
```
|
```
|
||||||
|
|
||||||
### Drop in self-hosted fonts
|
### Drop in self-hosted fonts (optional but recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -L -o apps/web/public/fonts/Geist-Variable.woff2 \
|
curl -L -o apps/web/public/fonts/Geist-Variable.woff2 \
|
||||||
@@ -161,84 +138,60 @@ curl -L -o apps/web/public/fonts/GeistMono-Variable.woff2 \
|
|||||||
https://github.com/vercel/geist-font/raw/main/fonts/GeistMonoVF.woff2
|
https://github.com/vercel/geist-font/raw/main/fonts/GeistMonoVF.woff2
|
||||||
```
|
```
|
||||||
|
|
||||||
(The `@font-face` rules are already in `apps/web/src/styles/global.css`.)
|
The `@font-face` rules are already in `apps/web/src/styles/global.css`.
|
||||||
|
|
||||||
### First boot
|
### First boot
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
docker compose logs -f cms # watch the first migration
|
docker compose logs -f softvowels-cms
|
||||||
```
|
```
|
||||||
|
|
||||||
The first time Payload starts it will print a one-time setup URL in the `cms` logs:
|
The first time Payload starts it prints a one-time URL for the first admin user. Find it with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs cms | grep -i "first user"
|
docker compose logs softvowels-cms | grep -i "first user"
|
||||||
```
|
```
|
||||||
|
|
||||||
Open it in your browser to create the first admin user, then start publishing at `https://cms.softvowels.example/admin`.
|
Open the URL in your browser to create the first admin user, then visit `https://cms.softvowels.example/admin` for the admin UI.
|
||||||
|
|
||||||
## On-demand rebuilds (webhook flow)
|
### Wipe everything (clean start)
|
||||||
|
|
||||||
The static site has to be rebuilt whenever content changes. The flow is:
|
|
||||||
|
|
||||||
```
|
|
||||||
Editor hits Publish in /admin
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Payload afterChange hook (apps/cms/src/hooks/revalidateAstro.ts)
|
|
||||||
│ POST http://host.docker.internal:4321/__revalidate?secret=…&collection=…&op=…
|
|
||||||
▼
|
|
||||||
scripts/webhook.js (running on the host, not in Docker)
|
|
||||||
│ spawns scripts/rebuild.sh
|
|
||||||
▼
|
|
||||||
scripts/rebuild.sh
|
|
||||||
│ git pull origin main → pnpm install → pnpm --filter web build
|
|
||||||
▼
|
|
||||||
The next browser request gets the fresh dist/ via the web container
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install the webhook on the host
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo cp scripts/softvowels-webhook.service /etc/systemd/system/
|
docker compose down -v
|
||||||
sudo systemctl edit --full softvowels-webhook.service
|
docker volume ls -q | grep softvowels | xargs -r docker volume rm
|
||||||
# (set WEBHOOK_PORT, REBUILD_TOKEN, REPO_DIR, LOG_FILE in [Service])
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable --now softvowels-webhook
|
|
||||||
sudo systemctl status softvowels-webhook
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `REBUILD_TOKEN` value here **must match** the one in `.env`. The webhook listens on `127.0.0.1:4321`; Payload reaches it via `host.docker.internal:4321` (already wired in `docker-compose.yml`).
|
## Updating the site after a content change
|
||||||
|
|
||||||
Logs land at `/var/log/softvowels-rebuild.log`. Tail with:
|
Until the on-publish webhook is wired up, the rebuild flow is manual:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo journalctl -u softvowels-webhook -f
|
cd /opt/softvowels
|
||||||
tail -f /var/log/softvowels-rebuild.log
|
git pull
|
||||||
|
docker compose build softvowels-web
|
||||||
|
docker compose up -d softvowels-web
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This rebuilds only the static site image and restarts the `softvowels-web` container. The CMS does not need to be rebuilt for content changes — only the web image does.
|
||||||
|
|
||||||
## Nginx Proxy Manager setup
|
## Nginx Proxy Manager setup
|
||||||
|
|
||||||
NPM runs on the host. Add two proxied hosts:
|
NPM runs on the host. The three softvowels containers are already attached to the `nginx_proxy_manager` external network. Add two proxied hosts:
|
||||||
|
|
||||||
| Domain | Forward to | Notes |
|
| Domain | Forward to | Scheme | Websockets |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| `softvowels.example` | `web:80` | Force SSL, enable HSTS, HTTP/2, websockets off |
|
| `softvowels.example` | `softvowels-web:80` | http | off |
|
||||||
| `cms.softvowels.example` | `cms:3000` | Force SSL, HSTS, websockets on (Payload admin uses them) |
|
| `cms.softvowels.example` | `softvowels-cms:3000` | http | on (Payload admin uses them) |
|
||||||
|
|
||||||
Because the `web` and `cms` services are on a private Docker network named `app`, NPM must run on the **same host** (or be given network access — easiest is host networking or adding NPM's container to the `app` network).
|
Both need: "Force SSL" + HSTS enabled.
|
||||||
|
|
||||||
If NPM is also in Docker, attach it:
|
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:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker network connect softvowels-app <npm_container_name>
|
docker network connect nginx_proxy_manager <npm-container-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Useful NPM custom locations (optional)
|
|
||||||
|
|
||||||
The `web` container already redirects `/admin` to the CMS. If you want NPM to handle that instead, add a `location /admin` redirect in NPM's "Advanced" tab.
|
|
||||||
|
|
||||||
## Customising the design
|
## Customising the design
|
||||||
|
|
||||||
All tokens are declared in `apps/web/src/styles/global.css` inside the `@theme` block. Change once, applies everywhere:
|
All tokens are declared in `apps/web/src/styles/global.css` inside the `@theme` block. Change once, applies everywhere:
|
||||||
@@ -255,6 +208,12 @@ Article prose styles live in `apps/web/src/styles/prose.css` (`.prose-article`).
|
|||||||
|
|
||||||
The full design brief is in `.opencode/stitch/DESIGN.md` and the original Stitch HTML mockups in `.opencode/stitch/softvowels_*/code.html`.
|
The full design brief is in `.opencode/stitch/DESIGN.md` and the original Stitch HTML mockups in `.opencode/stitch/softvowels_*/code.html`.
|
||||||
|
|
||||||
|
After changing design tokens, rebuild the web image (it bakes the styles at build time):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build softvowels-web && docker compose up -d softvowels-web
|
||||||
|
```
|
||||||
|
|
||||||
## Self-hosting fonts
|
## Self-hosting fonts
|
||||||
|
|
||||||
Drop the two variable woff2 files into `apps/web/public/fonts/`:
|
Drop the two variable woff2 files into `apps/web/public/fonts/`:
|
||||||
@@ -264,7 +223,7 @@ Drop the two variable woff2 files into `apps/web/public/fonts/`:
|
|||||||
| `Geist-Variable.woff2` | <https://github.com/vercel/geist-font> (rename `GeistVF.woff2`) |
|
| `Geist-Variable.woff2` | <https://github.com/vercel/geist-font> (rename `GeistVF.woff2`) |
|
||||||
| `GeistMono-Variable.woff2` | same repo (rename `GeistMonoVF.woff2`) |
|
| `GeistMono-Variable.woff2` | same repo (rename `GeistMonoVF.woff2`) |
|
||||||
|
|
||||||
The `@font-face` rules are already declared in `global.css`. The Material Symbols font is delivered through `@iconify-json/material-symbols` (svg sprites), so no separate font binary is required.
|
The `@font-face` rules are already declared in `global.css`. The Material Symbols font is delivered through `@iconify-json/material-symbols` (SVG sprites), so no separate font binary is required.
|
||||||
|
|
||||||
## Content model
|
## Content model
|
||||||
|
|
||||||
@@ -284,52 +243,47 @@ The Astro side renders Lexical richText to HTML using `@payloadcms/richtext-lexi
|
|||||||
## Useful commands
|
## Useful commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # Astro only
|
docker compose up -d --build # build and start all services
|
||||||
pnpm dev:cms # Payload only
|
docker compose down # stop (keeps volumes)
|
||||||
pnpm build # Astro build
|
docker compose down -v # stop and wipe volumes
|
||||||
pnpm build:cms # Payload build
|
docker compose logs -f softvowels-web # tail web container
|
||||||
pnpm typecheck # astro check (zero errors expected)
|
docker compose logs -f softvowels-cms # tail Payload
|
||||||
pnpm format # prettier --write
|
docker compose restart softvowels-web # restart just the web
|
||||||
pnpm format:check # prettier --check
|
docker compose build softvowels-web # rebuild just the web image
|
||||||
|
docker volume ls | grep softvowels # list softvowels volumes
|
||||||
docker compose up -d --build # build and start all services
|
|
||||||
docker compose down # stop (keeps volumes)
|
|
||||||
docker compose down -v # stop and wipe volumes
|
|
||||||
docker compose logs -f web # tail web container
|
|
||||||
docker compose logs -f cms # tail Payload
|
|
||||||
|
|
||||||
sudo journalctl -u softvowels-webhook -f # tail webhook
|
|
||||||
tail -f /var/log/softvowels-rebuild.log # tail rebuilds
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**`pnpm install` complains about a broken `pnpm` binary on Windows.** Use `npm` inside each app folder instead:
|
**`softvowels-cms` keeps restarting with `ERR_PNPM_NO_SCRIPT_OR_SERVER`.** The CMS uses Next.js standalone output (`next.config.ts: output: 'standalone'`). The runtime stage of `apps/cms/Dockerfile` runs `node server.js` directly — make sure the standalone build was actually produced (`docker compose build --no-cache softvowels-cms`).
|
||||||
```bash
|
|
||||||
cd apps/web && npm install
|
|
||||||
cd ../cms && npm install
|
|
||||||
```
|
|
||||||
`pnpm-workspace.yaml` works with both package managers for development. The homeserver `scripts/rebuild.sh` prefers pnpm if available, falls back to npm.
|
|
||||||
|
|
||||||
**`astro build` fails with `Cannot convert undefined or null to object`.** Make sure `tailwindcss` and `@tailwindcss/oxide` are pinned to the **same** version (we use 4.1.13). Mismatched versions cause the scanner to crash on Windows + Node 22.
|
**`/admin` returns 502.** The CMS container hasn't finished initialising yet. Wait ~30 s after `docker compose up`. The first boot runs Payload's database migrations; subsequent boots are fast.
|
||||||
|
|
||||||
**`/admin` returns 502.** The CMS container hasn't finished initialising yet. Wait ~30 s after `docker compose up`. The first boot runs Payload's database migrations.
|
**Editor in `/admin` can publish but the public site doesn't update.** That's expected — the on-publish webhook isn't wired up yet. Run the manual rebuild flow:
|
||||||
|
|
||||||
**`/admin` returns 404 from the `web` container.** It should redirect to the CMS. The redirect is configured in `apps/web/nginx-default.conf` (`location /admin`). If you've overridden NPM's locations, set the redirect there instead.
|
|
||||||
|
|
||||||
**Webhook returns `401`.** `REBUILD_TOKEN` in `apps/web/.env`, `apps/cms/.env`, `/etc/systemd/system/softvowels-webhook.service`, and `docker-compose.yml` (passed through as `ASTRO_REVALIDATE_TOKEN` to the CMS) must all match.
|
|
||||||
|
|
||||||
**Webhook returns `202` but nothing rebuilds.** Check `/var/log/softvowels-rebuild.log`. Common causes: `git pull` failed (auth), `pnpm install` failed (lockfile drift), or the rebuild script exited with an error before `pnpm build` completed.
|
|
||||||
|
|
||||||
**Editor in `/admin` can publish but the public site doesn't update.** The `web` container is a plain `nginx:1.27-alpine` that bind-mounts `./apps/web/dist` from the host. The webhook should be rewriting that folder. Verify:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ls -la apps/web/dist/index.html
|
cd /opt/softwovls
|
||||||
docker compose exec web ls -la /usr/share/nginx/html/index.html
|
git pull
|
||||||
|
docker compose build softvowels-web
|
||||||
|
docker compose up -d softvowels-web
|
||||||
```
|
```
|
||||||
|
|
||||||
If the host file is newer but the container still serves old content, run `docker compose restart web` so nginx re-opens the file handles. (nginx should pick up file changes automatically via inotify, but a restart is the nuclear option.)
|
**Public site returns 403 directory index forbidden.** The `softvowels-web` image wasn't built, or the build failed silently. Run `docker compose build --no-cache softvowels-web` and look for the `astro build` step output.
|
||||||
|
|
||||||
|
**Want to inspect the database.** Connect to it from any other container on the `nginx_proxy_manager` network:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -it --network nginx_proxy_manager postgres:16-alpine \
|
||||||
|
psql postgres://payload:$POSTGRES_PASSWORD@softvowels-db:5432/softvowels
|
||||||
|
```
|
||||||
|
|
||||||
|
**`/admin` first-user URL is in the past.** Payload's one-time URL expires. Restart the CMS to get a new one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose restart softvowels-cms
|
||||||
|
docker compose logs softvowels-cms | grep -i "first user"
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Built with Astro 5, PayloadCMS 3, Tailwind v4, and Postgres 16.
|
Built with Astro 5, PayloadCMS 3, Tailwind v4, and Postgres 16. Runs in Docker.
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.cache
|
||||||
|
media
|
||||||
|
uploads
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.log
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Payload
|
# Payload (used for local dev via `pnpm dev:cms`)
|
||||||
PAYLOAD_SECRET=change-me-to-a-long-random-string
|
PAYLOAD_SECRET=change-me-to-a-long-random-string
|
||||||
DATABASE_URI=postgres://payload:payload@localhost:5432/softvowels
|
DATABASE_URI=postgres://payload:payload@localhost:5432/softvowels
|
||||||
|
|
||||||
@@ -9,8 +9,4 @@ SERVER_URL=https://cms.softvowels.example
|
|||||||
# Origins allowed to call the API (Astro public site)
|
# Origins allowed to call the API (Astro public site)
|
||||||
CORS_ORIGINS=https://softvowels.example,http://localhost:4321
|
CORS_ORIGINS=https://softvowels.example,http://localhost:4321
|
||||||
|
|
||||||
# Internal rebuild webhook
|
|
||||||
ASTRO_REVALIDATE_URL=http://host.docker.internal:4321/__revalidate
|
|
||||||
ASTRO_REVALIDATE_TOKEN=change-me
|
|
||||||
|
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|||||||
+28
-8
@@ -1,17 +1,23 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
# syntax=docker/dockerfile:1.7
|
||||||
#
|
#
|
||||||
# PayloadCMS production image. Built and run by docker-compose.
|
# PayloadCMS production image. Built and run by docker-compose.
|
||||||
# We install pnpm globally via npm so we don't depend on corepack's
|
#
|
||||||
# (sometimes strict) version negotiation.
|
# We use Next.js `output: 'standalone'` so the runtime stage doesn't have
|
||||||
|
# to ship the entire node_modules tree — Next analyses the code, picks
|
||||||
|
# only the files that are actually required at runtime, and bundles them
|
||||||
|
# under .next/standalone/. This sidesteps the pnpm content-addressable
|
||||||
|
# store / bin-link issues we hit with the workspace install.
|
||||||
|
#
|
||||||
|
# We install pnpm via `npm install -g pnpm@9` instead of corepack so
|
||||||
|
# the version negotiation is unambiguous across Node 20 minor releases.
|
||||||
|
|
||||||
FROM node:20-alpine AS base
|
FROM node:20-alpine AS base
|
||||||
RUN npm install -g pnpm@9
|
RUN npm install -g pnpm@9
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
|
|
||||||
# ---- deps -----------------------------------------------------------------
|
# ---- deps -----------------------------------------------------------------
|
||||||
# Install ALL workspace deps at the root so the `apps/cms` package can
|
# Install ALL workspace deps at the root so apps/cms can resolve its
|
||||||
# resolve its siblings (@payloadcms/*, next, react, etc.) and the root
|
# siblings (@payloadcms/*, next, react, etc.).
|
||||||
# tooling (prettier) is also available.
|
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
COPY pnpm-workspace.yaml package.json ./
|
COPY pnpm-workspace.yaml package.json ./
|
||||||
COPY apps/cms/package.json ./apps/cms/package.json
|
COPY apps/cms/package.json ./apps/cms/package.json
|
||||||
@@ -29,11 +35,25 @@ FROM deps AS build
|
|||||||
COPY apps/cms ./apps/cms
|
COPY apps/cms ./apps/cms
|
||||||
WORKDIR /repo/apps/cms
|
WORKDIR /repo/apps/cms
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
# Some apps don't ship a `public/` dir; ensure one exists so the
|
||||||
|
# runtime COPY below doesn't fail.
|
||||||
|
RUN mkdir -p /repo/apps/cms/public
|
||||||
|
|
||||||
# ---- runtime --------------------------------------------------------------
|
# ---- runtime --------------------------------------------------------------
|
||||||
FROM base AS runtime
|
# Compose the self-contained runtime image from the standalone output.
|
||||||
|
# - .next/standalone/ contains the server.js entrypoint and a trimmed
|
||||||
|
# node_modules tree with only the files Next determined are needed.
|
||||||
|
# - .next/static/ is the hashed client-side bundles; standalone
|
||||||
|
# does NOT include it, so we copy it over manually.
|
||||||
|
# - public/ static assets served from the web root (may be
|
||||||
|
# absent; the COPY is guarded).
|
||||||
|
FROM node:20-alpine AS runtime
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
WORKDIR /repo/apps/cms
|
WORKDIR /repo/apps/cms
|
||||||
COPY --from=build /repo ./
|
COPY --from=build /repo/apps/cms/.next/standalone ./
|
||||||
|
COPY --from=build /repo/apps/cms/.next/static ./.next/static
|
||||||
|
COPY --from=build /repo/apps/cms/public ./public
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["pnpm", "start"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { NextConfig } from 'next';
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
output: 'standalone',
|
||||||
experimental: {
|
experimental: {
|
||||||
reactCompiler: false,
|
reactCompiler: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.astro
|
||||||
|
.cache
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.log
|
||||||
@@ -5,7 +5,4 @@ PUBLIC_SITE_URL=https://softvowels.example
|
|||||||
PUBLIC_PAYLOAD_API=https://cms.softvowels.example
|
PUBLIC_PAYLOAD_API=https://cms.softvowels.example
|
||||||
|
|
||||||
# Internal Payload base URL used at build time (container-to-container)
|
# Internal Payload base URL used at build time (container-to-container)
|
||||||
PAYLOAD_API_INTERNAL=http://cms:3000
|
PAYLOAD_API_INTERNAL=http://softvowels-cms:3000
|
||||||
|
|
||||||
# Token required to call the /__revalidate webhook
|
|
||||||
REVALIDATE_TOKEN=change-me
|
|
||||||
|
|||||||
+11
-9
@@ -1,15 +1,17 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
# syntax=docker/dockerfile:1.7
|
||||||
#
|
#
|
||||||
# Single-shot build image for the Astro site. The production deployment uses
|
# Astro static site production image. Built and run by docker-compose.
|
||||||
# nginx:1.27-alpine with a bind mount to apps/web/dist (see docker-compose.yml),
|
|
||||||
# so the webhook can rewrite the site without rebuilding any image.
|
|
||||||
#
|
#
|
||||||
# You only need this Dockerfile if you want a self-contained static site
|
# Multi-stage:
|
||||||
# image (e.g. for a registry). For the normal homeserver flow, this image
|
# deps - installs the workspace (only apps/web and apps/cms are
|
||||||
# is never built.
|
# needed for the web build, but we keep apps/cms/package.json
|
||||||
|
# so the workspace resolves cleanly).
|
||||||
|
# build - runs `astro build` to produce apps/web/dist.
|
||||||
|
# runtime - small nginx:1.27-alpine serving the baked-in dist/ and
|
||||||
|
# the custom nginx-default.conf.
|
||||||
#
|
#
|
||||||
# We install pnpm globally via npm to avoid corepack's strict version
|
# We install pnpm via `npm install -g pnpm@9` instead of corepack so
|
||||||
# negotiation across Node 20 minor versions.
|
# the version negotiation is unambiguous across Node 20 minor releases.
|
||||||
|
|
||||||
FROM node:20-alpine AS base
|
FROM node:20-alpine AS base
|
||||||
RUN npm install -g pnpm@9
|
RUN npm install -g pnpm@9
|
||||||
@@ -30,7 +32,7 @@ RUN if [ -f pnpm-lock.yaml ]; then \
|
|||||||
FROM deps AS build
|
FROM deps AS build
|
||||||
ENV PUBLIC_PAYLOAD_API=https://cms.softvowels.example
|
ENV PUBLIC_PAYLOAD_API=https://cms.softvowels.example
|
||||||
ENV PUBLIC_SITE_URL=https://softvowels.example
|
ENV PUBLIC_SITE_URL=https://softvowels.example
|
||||||
ENV PAYLOAD_API_INTERNAL=http://cms:3000
|
ENV PAYLOAD_API_INTERNAL=http://softvowels-cms:3000
|
||||||
COPY apps/web ./apps/web
|
COPY apps/web ./apps/web
|
||||||
WORKDIR /repo/apps/web
|
WORKDIR /repo/apps/web
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ const {
|
|||||||
const base =
|
const base =
|
||||||
'inline-flex items-center justify-center gap-2 font-label-md rounded-md transition-all duration-200 select-none disabled:opacity-50 disabled:cursor-not-allowed active:scale-[0.98]';
|
'inline-flex items-center justify-center gap-2 font-label-md rounded-md transition-all duration-200 select-none disabled:opacity-50 disabled:cursor-not-allowed active:scale-[0.98]';
|
||||||
|
|
||||||
const sizes: Record = {
|
const sizes: Record<Size, string> = {
|
||||||
sm: 'h-9 px-4 text-[13px]',
|
sm: 'h-9 px-4 text-[13px]',
|
||||||
md: 'h-10 px-5 text-sm',
|
md: 'h-10 px-5 text-sm',
|
||||||
lg: 'h-12 px-7 text-label-md',
|
lg: 'h-12 px-7 text-label-md',
|
||||||
};
|
};
|
||||||
|
|
||||||
const variants: Record = {
|
const variants: Record<Variant, string> = {
|
||||||
primary: 'bg-primary text-white hover:brightness-110',
|
primary: 'bg-primary text-white hover:brightness-110',
|
||||||
secondary: 'bg-white text-on-surface border border-border-subtle hover:border-primary',
|
secondary: 'bg-white text-on-surface border border-border-subtle hover:border-primary',
|
||||||
ghost: 'text-primary hover:bg-primary/5',
|
ghost: 'text-primary hover:bg-primary/5',
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ const {
|
|||||||
ariaLabel,
|
ariaLabel,
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
const surfaces: Record = {
|
const surfaces: Record<NonNullable<Props['variant']>, string> = {
|
||||||
raised: 'bg-surface-raised',
|
raised: 'bg-surface-raised',
|
||||||
flat: 'bg-surface-container',
|
flat: 'bg-surface-container',
|
||||||
};
|
};
|
||||||
const pads: Record = {
|
const pads: Record<NonNullable<Props['padding']>, string> = {
|
||||||
none: '',
|
none: '',
|
||||||
sm: 'p-4',
|
sm: 'p-4',
|
||||||
md: 'p-6',
|
md: 'p-6',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface Props {
|
|||||||
const { variant = 'solid', href, class: className = '', ariaLabel } = Astro.props;
|
const { variant = 'solid', href, class: className = '', ariaLabel } = Astro.props;
|
||||||
const base =
|
const base =
|
||||||
'inline-flex items-center gap-1.5 rounded-full font-label-md tracking-wider uppercase text-[11px] px-2.5 py-1 transition-colors duration-200';
|
'inline-flex items-center gap-1.5 rounded-full font-label-md tracking-wider uppercase text-[11px] px-2.5 py-1 transition-colors duration-200';
|
||||||
const variants: Record = {
|
const variants: Record<Variant, string> = {
|
||||||
solid: 'bg-primary text-white',
|
solid: 'bg-primary text-white',
|
||||||
outline: 'border border-primary/40 text-primary',
|
outline: 'border border-primary/40 text-primary',
|
||||||
soft: 'bg-primary/10 text-primary',
|
soft: 'bg-primary/10 text-primary',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { name, size = 'md', class: className = '', filled = false, ariaLabel } = Astro.props;
|
const { name, size = 'md', class: className = '', filled = false, ariaLabel } = Astro.props;
|
||||||
const sizes: Record = {
|
const sizes: Record<NonNullable<Props['size']>, string> = {
|
||||||
sm: 'text-[18px]',
|
sm: 'text-[18px]',
|
||||||
md: 'text-[20px]',
|
md: 'text-[20px]',
|
||||||
lg: 'text-[24px]',
|
lg: 'text-[24px]',
|
||||||
|
|||||||
+19
-20
@@ -4,19 +4,23 @@ name: softvowels
|
|||||||
# SoftVowels — production docker-compose
|
# SoftVowels — production docker-compose
|
||||||
#
|
#
|
||||||
# This stack publishes a static site (Astro) and a PayloadCMS
|
# This stack publishes a static site (Astro) and a PayloadCMS
|
||||||
# admin panel. Both attach to the homeserver's existing
|
# admin panel. All three containers attach to the homeserver's
|
||||||
# `nginx_proxy_manager` network so Nginx Proxy Manager can
|
# existing `nginx_proxy_manager` network so Nginx Proxy Manager
|
||||||
# reverse-proxy them with TLS.
|
# can reverse-proxy them with TLS.
|
||||||
#
|
#
|
||||||
# - softvowels-db Postgres 16
|
# - softvowels-db Postgres 16
|
||||||
# - softvowels-cms Payload admin + REST/GraphQL API (Node 20)
|
# - softvowels-cms Payload admin + REST/GraphQL API (Node 20,
|
||||||
# - softvowels-web nginx serving the static site (Astro build)
|
# Next.js standalone output)
|
||||||
|
# - softvowels-web nginx serving the static site (Astro build,
|
||||||
|
# dist baked into the image)
|
||||||
#
|
#
|
||||||
# How to access:
|
# How to access (after `docker compose up -d --build`):
|
||||||
# 1. https://softvowels.example -> softvowels-web:80
|
# 1. https://softvowels.example -> softvowels-web:80
|
||||||
# 2. https://cms.softvowels.example -> softvowels-cms:3000/admin
|
# 2. https://cms.softvowels.example -> softvowels-cms:3000/admin
|
||||||
# Add both as "Proxy Hosts" in Nginx Proxy Manager, pointing
|
# Add both as "Proxy Hosts" in Nginx Proxy Manager, pointing to
|
||||||
# to the matching container name + port. See README.
|
# the matching container name + port. See README.
|
||||||
|
#
|
||||||
|
# Host requirements: Docker only. No Node, no pnpm, no git needed.
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -55,27 +59,22 @@ services:
|
|||||||
PAYLOAD_PUBLIC_SERVER_URL: https://${CMS_HOSTNAME}
|
PAYLOAD_PUBLIC_SERVER_URL: https://${CMS_HOSTNAME}
|
||||||
SERVER_URL: https://${CMS_HOSTNAME}
|
SERVER_URL: https://${CMS_HOSTNAME}
|
||||||
CORS_ORIGINS: https://${WEB_HOSTNAME},http://localhost:4321
|
CORS_ORIGINS: https://${WEB_HOSTNAME},http://localhost:4321
|
||||||
ASTRO_REVALIDATE_URL: http://host.docker.internal:4321/__revalidate
|
# media uploads persist in a named volume mounted at the same
|
||||||
ASTRO_REVALIDATE_TOKEN: ${REVALIDATE_TOKEN}
|
# path Next standalone's CWD expects. Payload's Media collection
|
||||||
|
# uses `staticDir: 'media'` relative to apps/cms.
|
||||||
volumes:
|
volumes:
|
||||||
- softvowels-cms-uploads:/app/apps/cms/media
|
- softvowels-cms-uploads:/repo/apps/cms/media
|
||||||
networks:
|
networks:
|
||||||
- nginx_proxy_manager
|
- nginx_proxy_manager
|
||||||
|
|
||||||
# The public site. We deliberately do NOT bake dist/ into the image:
|
|
||||||
# scripts/rebuild.sh (run by the host-side webhook) writes fresh HTML to
|
|
||||||
# ./apps/web/dist on the host, and we mount that path into nginx. That way
|
|
||||||
# publishing in /admin → new content is live within ~30-60s without any
|
|
||||||
# docker build / image push step.
|
|
||||||
softvowels-web:
|
softvowels-web:
|
||||||
image: nginx:1.27-alpine
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./apps/web/Dockerfile
|
||||||
container_name: softvowels-web
|
container_name: softvowels-web
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- softvowels-cms
|
- softvowels-cms
|
||||||
volumes:
|
|
||||||
- ./apps/web/dist:/usr/share/nginx/html:ro
|
|
||||||
- ./apps/web/nginx-default.conf:/etc/nginx/conf.d/default.conf:ro
|
|
||||||
networks:
|
networks:
|
||||||
- nginx_proxy_manager
|
- nginx_proxy_manager
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user