cms: add openbureau-core CMS (DB-less, auth: local) — vendored at cms/core
kgva can now be edited through openbureau-core instead of Decap: a small Node CMS (git subtree at cms/core) with a schema-driven editor at /admin, file-based login (no Supabase/Postgres), Markdown on disk. cms/kgva.config.js models the content (portfolio/pages/sections); docker-compose builds the core image (Hugo 0.163.3, no VITE_SUPABASE_URL → local-auth admin) and mounts the repo as the site. seed an admin via cms/seed-admin.mjs; deploy/provision-lxc.sh + ADMIN.md updated. Verified DB-less end-to-end on real content (staged): local login → schema-driven editor → 42 entries (de/en) → stats → create/preview/cleanup; unknown frontmatter (image galleries) preserved on save. The LIVE static site (CT130 timer→hugo→nginx) is unaffected — hugo ignores cms/; the CMS is dormant until provisioned. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
# CMS (db-less) secrets + users + build
|
||||||
|
cms/.env
|
||||||
|
cms/users.json
|
||||||
|
public/
|
||||||
|
resources/
|
||||||
@@ -1,68 +1,36 @@
|
|||||||
# Admin (Decap CMS)
|
# Admin (openbureau-core CMS)
|
||||||
|
|
||||||
A lightweight, git-based editor at `/admin`. It commits markdown straight to the
|
The site is edited through **openbureau-core** — a small Node CMS (vendored at
|
||||||
Gitea repo — no database, no server. The public site stays static; rebuilding it
|
`cms/core`) that builds the Hugo site and serves it together with an editor at
|
||||||
after an edit is a separate step (see *Deploy* below).
|
`/admin`. It is **DB-less**: file-based login (`auth: 'local'`) and Markdown on
|
||||||
|
disk. No Supabase, no Postgres.
|
||||||
|
|
||||||
## 1. Put the repo on Gitea
|
## What's where
|
||||||
|
|
||||||
The site must live in a Gitea repo (e.g. `git.kgva.ch/karim/kgva`). Set the same
|
- `cms/kgva.config.js` — the content model (collections + fields) the editor renders.
|
||||||
`repo` and `branch` in `static/admin/config.yml`.
|
- `cms/core/` — the engine (git subtree of `git.openbureau.ch/karim/openbureau-core`).
|
||||||
|
Update it with:
|
||||||
|
`git subtree pull --prefix=cms/core https://git.openbureau.ch/karim/openbureau-core.git main --squash`
|
||||||
|
- `cms/.env` — `JWT_SECRET` (sign-in secret) + `ADMIN_EMAILS`. Copy from `.env.example`.
|
||||||
|
- `cms/users.json` — the user store (bcrypt hashes). Git-ignored. Seed with:
|
||||||
|
`docker compose run --rm cms node /site/cms/seed-admin.mjs you@mail yourpass`
|
||||||
|
|
||||||
## 2. Create a Gitea OAuth app
|
## Run
|
||||||
|
|
||||||
In Gitea: **Settings → Applications → Create OAuth2 application**.
|
|
||||||
|
|
||||||
- Redirect URI: `https://<your-site>/admin/`
|
|
||||||
- Copy the **Client ID** into `static/admin/config.yml` → `backend.app_id`.
|
|
||||||
|
|
||||||
Decap uses PKCE, so no client secret and no auth proxy are needed.
|
|
||||||
|
|
||||||
## 3. Edit
|
|
||||||
|
|
||||||
Open `https://<your-site>/admin/`, log in with Gitea. New project:
|
|
||||||
*Portfolio → New Project*; images upload into `static/img` and are referenced as
|
|
||||||
`/img/...`. Saving commits to the repo on the configured branch.
|
|
||||||
|
|
||||||
## 4. Deploy (rebuild after an edit)
|
|
||||||
|
|
||||||
Because the site is baked into the Docker image, a commit must trigger a rebuild.
|
|
||||||
Pick one:
|
|
||||||
|
|
||||||
- **Manual:** on the host, `docker compose up -d --build`.
|
|
||||||
- **Gitea Actions** (`.gitea/workflows/deploy.yml`), sketch:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
on: { push: { branches: [main] } }
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- run: docker compose up -d --build # on a runner with access to the host
|
|
||||||
```
|
|
||||||
|
|
||||||
(Adjust to your Proxmox/LXC setup — same place OPENBUREAU deploys.)
|
|
||||||
|
|
||||||
- **Webhook:** point a Gitea webhook at a tiny script on the host that runs the
|
|
||||||
`docker compose` line.
|
|
||||||
|
|
||||||
## Local editing (no auth)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
npx decap-server # in one terminal
|
cp cms/.env.example cms/.env # set JWT_SECRET (openssl rand -hex 32)
|
||||||
# uncomment `local_backend: true` in config.yml
|
docker compose build
|
||||||
hugo server # in another
|
docker compose run --rm cms node /site/cms/seed-admin.mjs karim@gabrielevarano.ch <pw> # once
|
||||||
# open http://localhost:1313/admin/
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## Self-hosting the CMS bundle (optional)
|
Open `http://<host>:8080/admin/`, log in, edit. *Speichern* writes the Markdown,
|
||||||
|
*Vorschau* builds a draft preview, *Publizieren* rebuilds the public site.
|
||||||
|
|
||||||
To avoid the CDN (in the spirit of the colophon), download the bundle once and
|
## Deploy
|
||||||
point `static/admin/index.html` at it:
|
|
||||||
|
|
||||||
```
|
A single container (Hugo build + Node serve). See `deploy/` —
|
||||||
curl -L https://unpkg.com/decap-cms@^3.3.3/dist/decap-cms.js \
|
`provision-lxc.sh` stands up an LXC and does all of the above (incl. a seeded
|
||||||
-o static/admin/decap-cms.js
|
admin whose password lands in `cms/ADMIN_PASSWORD.txt`); `Caddyfile` terminates
|
||||||
# then in index.html: <script src="/admin/decap-cms.js"></script>
|
TLS and reverse-proxies the domain. Safe cutover: provision a NEW container on a
|
||||||
```
|
temporary IP, verify, point Caddy at it, then retire the old one.
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# kgva CMS — DB-less (openbureau-core, auth: local). Copy to cms/.env, fill in.
|
||||||
|
# Sign-in token secret — random: openssl rand -hex 32
|
||||||
|
JWT_SECRET=CHANGE_ME
|
||||||
|
# Always-admin emails (comma-separated).
|
||||||
|
ADMIN_EMAILS=karim@gabrielevarano.ch
|
||||||
|
# Commit edits back to git on publish (optional).
|
||||||
|
GIT_PUBLISH=false
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// kgva site config — consumed by openbureau-core (vendored at cms/core) via
|
||||||
|
// CMS_CONFIG. DB-less: auth 'local' (file users), no plugins. Bilingual content
|
||||||
|
// (.de.md) lists as separate entries (edit each language file on its own).
|
||||||
|
export default {
|
||||||
|
site: 'kgva',
|
||||||
|
auth: 'local',
|
||||||
|
admins: ['karim@gabrielevarano.ch'],
|
||||||
|
plugins: [],
|
||||||
|
|
||||||
|
collections: [
|
||||||
|
{
|
||||||
|
kind: 'project', label: 'Portfolio', order: 0,
|
||||||
|
path: 'portfolio/:slug', statKey: 'portfolio',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'string', required: true },
|
||||||
|
{ name: 'slug', type: 'slug' },
|
||||||
|
{ name: 'date', type: 'date' },
|
||||||
|
{ name: 'draft', type: 'bool', default: true },
|
||||||
|
{ name: 'thumbnail', type: 'image' },
|
||||||
|
{ name: 'video', type: 'string', hint: '/media/x.mp4' },
|
||||||
|
{ name: 'studies', type: 'string', hint: '/studies/hslu/ba/semester_05' },
|
||||||
|
{ name: 'schools', type: 'list' },
|
||||||
|
{ name: 'degrees', type: 'list' },
|
||||||
|
{ name: 'semesters', type: 'list' },
|
||||||
|
{ name: 'body', type: 'markdown' },
|
||||||
|
// NB: image galleries (`images:`) and any other frontmatter not modelled
|
||||||
|
// here are PRESERVED on save (the editor keeps unknown keys).
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'page', label: 'Seiten', order: 1, fallback: true, statKey: 'pages',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'string', required: true },
|
||||||
|
{ name: 'description', type: 'string' },
|
||||||
|
{ name: 'layout', type: 'string' },
|
||||||
|
{ name: 'body', type: 'markdown' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'section', label: 'Index-Seiten', order: 2, index: true, statKey: 'sections',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'string' },
|
||||||
|
{ name: 'description', type: 'string' },
|
||||||
|
{ name: 'body', type: 'markdown' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Seed the first admin into the local users file (DB-less auth). Run once:
|
||||||
|
// docker compose run --rm cms node /site/cms/seed-admin.mjs you@mail yourpass
|
||||||
|
import { createUser, loadUsers } from '/app/src/auth-local.js';
|
||||||
|
const [email, password] = process.argv.slice(2);
|
||||||
|
if (!email || !password) { console.error('usage: seed-admin.mjs <email> <password>'); process.exit(1); }
|
||||||
|
if ((await loadUsers()).some((u) => (u.email || '').toLowerCase() === email.toLowerCase())) {
|
||||||
|
console.log('user already exists:', email); process.exit(0);
|
||||||
|
}
|
||||||
|
console.log('seeded admin', email, await createUser({ email, password, role: 'admin' }));
|
||||||
+16
-2
@@ -37,10 +37,24 @@ pct exec "$CTID" -- bash -lc '
|
|||||||
> /etc/apt/sources.list.d/docker.list
|
> /etc/apt/sources.list.d/docker.list
|
||||||
apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
rm -rf /opt/kgva && git clone '"$REPO"' /opt/kgva
|
rm -rf /opt/kgva && git clone '"$REPO"' /opt/kgva
|
||||||
cd /opt/kgva && docker compose up -d --build
|
cd /opt/kgva
|
||||||
|
# DB-less CMS env: random token secret (once).
|
||||||
|
[ -f cms/.env ] || sed "s#CHANGE_ME#$(openssl rand -hex 32)#" cms/.env.example > cms/.env
|
||||||
|
# the CMS runs as uid 1000 and must write content/ + build public/.
|
||||||
|
chown -R 1000:1000 /opt/kgva
|
||||||
|
docker compose build
|
||||||
|
# seed an admin on first provision; password saved to cms/ADMIN_PASSWORD.txt.
|
||||||
|
if [ ! -f cms/users.json ]; then
|
||||||
|
PW="$(openssl rand -base64 12)"
|
||||||
|
docker compose run --rm -T cms node /site/cms/seed-admin.mjs karim@gabrielevarano.ch "$PW"
|
||||||
|
printf "%s\n" "$PW" > cms/ADMIN_PASSWORD.txt
|
||||||
|
chown 1000:1000 cms/ADMIN_PASSWORD.txt cms/users.json
|
||||||
|
fi
|
||||||
|
docker compose up -d
|
||||||
'
|
'
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "done. test the new container: curl -I http://${IP%/*}:8080"
|
echo "done. CMS admin at http://${IP%/*}:8080/admin/ (login: karim@… / cms/ADMIN_PASSWORD.txt)"
|
||||||
|
echo "test: curl -I http://${IP%/*}:8080"
|
||||||
echo "then point Caddy at ${IP%/*}:8080 (see deploy/Caddyfile), reload caddy,"
|
echo "then point Caddy at ${IP%/*}:8080 (see deploy/Caddyfile), reload caddy,"
|
||||||
echo "verify the domain, and only THEN remove the old container."
|
echo "verify the domain, and only THEN remove the old container."
|
||||||
|
|||||||
+27
-5
@@ -1,8 +1,30 @@
|
|||||||
|
# kgva website + CMS, powered by openbureau-core (vendored at cms/core).
|
||||||
|
# DB-LESS: local file auth (no Supabase/Postgres). The core engine builds the Hugo
|
||||||
|
# site and serves it + /admin + /api + /_preview, writing content/ on edits.
|
||||||
|
#
|
||||||
|
# Provision: cp cms/.env.example cms/.env (set JWT_SECRET), then
|
||||||
|
# docker compose build
|
||||||
|
# docker compose run --rm cms node /site/cms/seed-admin.mjs you@mail pass # once
|
||||||
|
# docker compose up -d
|
||||||
services:
|
services:
|
||||||
web:
|
cms:
|
||||||
build: .
|
build:
|
||||||
image: kgva
|
context: ./cms/core
|
||||||
|
dockerfile: api/Dockerfile
|
||||||
|
args:
|
||||||
|
HUGO_VERSION: "0.163.3" # kgva needs >= 0.163.3
|
||||||
|
# No VITE_SUPABASE_URL → the admin uses the local-auth login (DB-less).
|
||||||
|
image: kgva-cms
|
||||||
container_name: kgva
|
container_name: kgva
|
||||||
ports:
|
|
||||||
- "8080:80"
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
CMS_CONFIG: /site/cms/kgva.config.js
|
||||||
|
SITE_DIR: /site
|
||||||
|
USERS_FILE: /site/cms/users.json
|
||||||
|
PORT: "3000"
|
||||||
|
env_file:
|
||||||
|
- cms/.env
|
||||||
|
volumes:
|
||||||
|
- .:/site
|
||||||
|
ports:
|
||||||
|
- "8080:3000"
|
||||||
|
|||||||
Reference in New Issue
Block a user