Document full-stack setup; fix dev proxy; drop stray temp script #2
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,3 +30,6 @@ Thumbs.db
|
|||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# scratch / temp scripts
|
||||||
|
_*.ps1
|
||||||
|
|||||||
273
README.md
273
README.md
@@ -1,63 +1,242 @@
|
|||||||
# UOMysticmoon Website
|
# UOMysticmoon Website
|
||||||
|
|
||||||
Public site, wiki, and protected admin panel for the UOMysticmoon private Ultima Online
|
Public site, wiki, and protected admin panel for the **UOMysticmoon** private Ultima Online
|
||||||
shard. Built on the `serverlinkr` layered pattern: **Express + MariaDB + JWT** backend and a
|
shard — a full-stack app in one repo:
|
||||||
**React + Vite** frontend in the same repo, deployed with **Docker Compose** behind a
|
|
||||||
**Pangolin** reverse proxy.
|
|
||||||
|
|
||||||
> Build order: **(1) backend** (this phase) → (2) frontend design (Claude Design) →
|
- **Backend** — Node.js + Express REST API (layered `router → controller → model → db`), MariaDB, JWT-in-cookie auth.
|
||||||
> (3) frontend coding. See [BACKEND_DESIGN.md](BACKEND_DESIGN.md) for the full design.
|
- **Frontend** — React + Vite single-page app (public site, wiki, and the admin panel), dark "gothic" theme (Cinzel + Georgia).
|
||||||
|
- **Deploy** — Docker Compose (app + MariaDB) behind a Pangolin reverse proxy. Express serves the built SPA in production.
|
||||||
|
|
||||||
## Layout
|
The design reference is [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (API contract, schema, security).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Tech stack](#tech-stack)
|
||||||
|
- [Project structure](#project-structure)
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Setup & run](#setup--run)
|
||||||
|
- [Option A — Docker Compose (full stack)](#option-a--docker-compose-full-stack)
|
||||||
|
- [Option B — Local development (hot reload)](#option-b--local-development-hot-reload)
|
||||||
|
- [Option C — Production build without Docker](#option-c--production-build-without-docker)
|
||||||
|
- [First admin & site mode](#first-admin--site-mode)
|
||||||
|
- [Pages & routes](#pages--routes)
|
||||||
|
- [API endpoints](#api-endpoints)
|
||||||
|
- [Environment variables](#environment-variables)
|
||||||
|
- [Security](#security)
|
||||||
|
- [Logging](#logging)
|
||||||
|
- [Deployment behind Pangolin](#deployment-behind-pangolin)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
| Layer | Tech |
|
||||||
|
|---|---|
|
||||||
|
| Backend | Node.js 20+, Express 4, `mariadb` driver (parameterized SQL, no ORM) |
|
||||||
|
| Auth | JWT in an httpOnly cookie, bcrypt password hashing |
|
||||||
|
| Database | MariaDB 11 (own container) |
|
||||||
|
| Frontend | React 18, Vite 5, React Router 6 |
|
||||||
|
| Email | Nodemailer (SMTP) with a `mailto:` fallback |
|
||||||
|
| Deploy | Docker Compose, Pangolin reverse proxy |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
```
|
```
|
||||||
server/ Express API (router → controller → model → db), MariaDB schema + seed
|
UOMSITE/
|
||||||
client/ React + Vite SPA (added in the frontend phase)
|
├─ server/ Express API
|
||||||
Dockerfile, docker-compose.yml, .env.example
|
│ ├─ src/
|
||||||
|
│ │ ├─ server.js bootstrap: ensure schema → seed → listen (0.0.0.0)
|
||||||
|
│ │ ├─ app.js middleware + static SPA + routes
|
||||||
|
│ │ ├─ router/v1/ auth / public / admin route groups
|
||||||
|
│ │ ├─ model/ users · posts · wiki · settings · activity (.model + .db)
|
||||||
|
│ │ ├─ middleware/ siteMode · noindex · rateLimit · validate
|
||||||
|
│ │ └─ utils/ auth (JWT/cookies) · db (pool) · mailer · logger
|
||||||
|
│ ├─ db/ schema.sql + seed.js
|
||||||
|
│ └─ .env.example
|
||||||
|
├─ client/ React + Vite SPA
|
||||||
|
│ ├─ src/
|
||||||
|
│ │ ├─ routes/public/ Portal, Website, News, Screenshots, FiveOnFriday, Newsletter(+Issue), Status, About, Maintenance
|
||||||
|
│ │ ├─ routes/wiki/ Wiki landing + WikiArticle
|
||||||
|
│ │ ├─ routes/admin/ AdminLogin, AdminLayout, views/ (Dashboard, Posts, Wiki, Settings, Activity, Users) + editors
|
||||||
|
│ │ ├─ components/ SiteHeader, SiteFooter, layout, guards, Modal, …
|
||||||
|
│ │ ├─ contexts/ AuthContext, SiteContext
|
||||||
|
│ │ ├─ api/client.js fetch wrapper (sends cookies)
|
||||||
|
│ │ └─ styles/theme.css design tokens
|
||||||
|
│ └─ public/assets/img/ hero image
|
||||||
|
├─ Dockerfile builds client → serves via Express
|
||||||
|
├─ docker-compose.yml app + MariaDB
|
||||||
|
├─ .env.example root env (used by Compose)
|
||||||
|
└─ package.json workspace scripts
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick start (local dev)
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Node.js 20+** and npm (Node 22/24 are fine).
|
||||||
|
- **Docker Desktop** (for MariaDB, and for the full Compose deploy).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup & run
|
||||||
|
|
||||||
|
### Option A — Docker Compose (full stack)
|
||||||
|
|
||||||
|
The simplest way to run everything. The image installs server deps, **builds the React client**,
|
||||||
|
and Express serves it; MariaDB runs in its own container; tables + defaults + the first admin are
|
||||||
|
created automatically on first boot.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Start a MariaDB (or use your own and set DB_* in server/.env)
|
cp .env.example .env
|
||||||
docker compose up -d db
|
# Edit .env and set at least:
|
||||||
|
# DB_PASSWORD, DB_ROOT_PASSWORD (any strong values)
|
||||||
|
# JWT_SECRET (a long random string)
|
||||||
|
# ADMIN_USERNAME, ADMIN_PASSWORD (your first admin login)
|
||||||
|
|
||||||
# 2. Configure + install
|
docker compose up -d --build
|
||||||
cp server/.env.example server/.env # edit DB_*, JWT_SECRET, ADMIN_USERNAME/PASSWORD
|
```
|
||||||
|
|
||||||
|
- App: **http://localhost:3000** (binds `0.0.0.0`)
|
||||||
|
- Health check: `GET http://localhost:3000/api/health` → `{ "status": "ok" }`
|
||||||
|
- Logs: `docker compose logs -f app` (and `./logs/app.log` on the host)
|
||||||
|
- Stop: `docker compose down` (add `-v` to also wipe the database + uploads volumes)
|
||||||
|
|
||||||
|
### Option B — Local development (hot reload)
|
||||||
|
|
||||||
|
Run the API and the Vite dev server separately. The Vite server proxies `/api` and `/uploads`
|
||||||
|
to the backend, so the SPA stays same-origin (cookies work).
|
||||||
|
|
||||||
|
**1. Start a MariaDB the backend can reach** (published on `localhost:3306`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name uomm-db -p 3306:3306 -e MARIADB_DATABASE=uomysticmoon -e MARIADB_USER=uomm -e MARIADB_PASSWORD=devpass -e MARIADB_ROOT_PASSWORD=rootpass mariadb:11
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Configure + start the backend** (terminal 1):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp server/.env.example server/.env
|
||||||
|
# Set DB_HOST=127.0.0.1, DB_PORT=3306, DB_USER=uomm, DB_PASSWORD=devpass,
|
||||||
|
# JWT_SECRET=<anything>, ADMIN_USERNAME=admin, ADMIN_PASSWORD=<your password>
|
||||||
npm run install-server
|
npm run install-server
|
||||||
|
npm run server # nodemon → http://localhost:3000
|
||||||
# 3. Run the API (creates tables, seeds defaults + first admin on boot)
|
|
||||||
npm run server # http://localhost:3000 (API at /api/v1)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`GET /api/health` → `{ "status": "ok" }` confirms it's up.
|
**3. Start the frontend** (terminal 2):
|
||||||
|
|
||||||
## Deploy (Docker Compose)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # fill in DB creds, JWT_SECRET, admin, SMTP
|
npm run install-client
|
||||||
docker compose up -d --build # app on 0.0.0.0:3000, MariaDB on the internal network
|
npm run client # Vite → http://localhost:5173
|
||||||
```
|
```
|
||||||
|
|
||||||
Point Pangolin at the `app` container on port 3000. The auth cookie auto-detects HTTPS, so
|
Develop at **http://localhost:5173** (hot reload). On Windows, the Vite proxy targets
|
||||||
the admin panel works both via the LAN IP (HTTP) and through the proxy (HTTPS). The full app
|
`127.0.0.1:3000` to avoid the IPv6-`localhost` pitfall.
|
||||||
image build requires `client/` (frontend phase); until then the server runs API-only.
|
|
||||||
|
|
||||||
## Key endpoints
|
> Tip: `npm run install-all` installs both server and client deps in one go.
|
||||||
|
|
||||||
|
### Option C — Production build without Docker
|
||||||
|
|
||||||
|
Build the SPA and let Express serve it on a single port (still needs a MariaDB + `server/.env`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run install-all
|
||||||
|
npm run build # → client/dist
|
||||||
|
npm start # node server → serves API + SPA at http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## First admin & site mode
|
||||||
|
|
||||||
|
- On first boot, if the `users` table is empty and `ADMIN_USERNAME` / `ADMIN_PASSWORD` are set,
|
||||||
|
the first admin is created automatically. You can also run `npm run seed`. After it exists you
|
||||||
|
may blank those env vars.
|
||||||
|
- The site **starts in `maintenance` mode**: public visitors see the polished "coming soon" page;
|
||||||
|
the admin login and panel are always reachable.
|
||||||
|
- Sign in at **`/admin/login`**, then flip **Maintenance → Live** from the Dashboard. A logged-in
|
||||||
|
admin can preview the live site even while it's in maintenance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pages & routes
|
||||||
|
|
||||||
|
**Public** (gated by site mode):
|
||||||
|
|
||||||
|
| Route | Page |
|
||||||
|
|---|---|
|
||||||
|
| `/` | Portal landing (hero + destinations) |
|
||||||
|
| `/site` | Website index (section cards) |
|
||||||
|
| `/site/news` | News feed |
|
||||||
|
| `/site/screenshots` | Screenshot gallery |
|
||||||
|
| `/site/five-on-friday` | Five on Friday |
|
||||||
|
| `/site/newsletter` · `/site/newsletter/:id` | Newsletter list + issue |
|
||||||
|
| `/site/about` · `/site/status` | About · Shard status |
|
||||||
|
| `/wiki` · `/wiki/:slug` | Wiki landing + article (auto table-of-contents) |
|
||||||
|
|
||||||
|
**Admin** (cookie auth, `noindex`):
|
||||||
|
|
||||||
|
| Route | View |
|
||||||
|
|---|---|
|
||||||
|
| `/admin/login` | Sign in |
|
||||||
|
| `/admin` | Dashboard (mode toggle, stats, recent activity) |
|
||||||
|
| `/admin/posts` | Posts CRUD + publish + image upload |
|
||||||
|
| `/admin/wiki` | Wiki pages CRUD |
|
||||||
|
| `/admin/settings` | Site settings |
|
||||||
|
| `/admin/activity` | Activity log |
|
||||||
|
| `/admin/users` | User management |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API endpoints
|
||||||
|
|
||||||
| Group | Base | Auth |
|
| Group | Base | Auth |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Auth | `/api/v1/auth` (`login`, `logout`, `me`) | cookie |
|
| Auth | `/api/v1/auth` (`login`, `logout`, `me`) | cookie |
|
||||||
| Public | `/api/v1/public` (`settings`, `status`, `posts/:category`, `wiki`, `contact`) | none |
|
| Public | `/api/v1/public` (`settings`, `status`, `posts/:category`, `posts/:category/:idOrSlug`, `wiki`, `wiki/:slug`, `contact`) | none |
|
||||||
| Admin | `/api/v1/admin` (dashboard, site-mode, posts, wiki, settings, activity, users) | cookie (admin) |
|
| Admin | `/api/v1/admin` (`dashboard`, `site-mode`, `posts`, `posts/upload`, `wiki`, `settings`, `activity`, `users`) | cookie (admin) |
|
||||||
|
|
||||||
See [BACKEND_DESIGN.md](BACKEND_DESIGN.md) §4 for the complete contract.
|
Post categories (URL form): `news`, `five-on-friday`, `newsletter`, `screenshots`.
|
||||||
|
See [BACKEND_DESIGN.md](BACKEND_DESIGN.md) §4 for the full contract.
|
||||||
|
|
||||||
## Security notes
|
---
|
||||||
|
|
||||||
JWT in an httpOnly cookie · bcrypt hashing · login rate limiting · admin routes `noindex` ·
|
## Environment variables
|
||||||
first admin seeded from env (no hardcoded credentials) · `.env` is git-ignored. SMTP is
|
|
||||||
optional — the contact form falls back to a `mailto:` link when SMTP is not configured.
|
Copy `.env.example` (Compose) or `server/.env.example` (local) and fill in. **`.env` is git-ignored.**
|
||||||
|
|
||||||
|
| Var | Default | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `NODE_ENV` | `production` | |
|
||||||
|
| `PORT` | `3000` | server listens on `0.0.0.0:PORT` |
|
||||||
|
| `DB_HOST` / `DB_PORT` | `db` / `3306` | `db` in Compose; `127.0.0.1` for local dev |
|
||||||
|
| `DB_NAME` / `DB_USER` / `DB_PASSWORD` | `uomysticmoon` / `uomm` / — | app database credentials |
|
||||||
|
| `DB_ROOT_PASSWORD` | — | MariaDB root (Compose only) |
|
||||||
|
| `JWT_SECRET` | — | **required** — long random string |
|
||||||
|
| `JWT_EXPIRES_IN` | `1d` | token + cookie lifetime |
|
||||||
|
| `COOKIE_SECURE` | `auto` | `auto` = Secure only over HTTPS (works on LAN HTTP + Pangolin HTTPS) |
|
||||||
|
| `COOKIE_NAME` | `uomm_token` | |
|
||||||
|
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | — | first-admin bootstrap (first boot only) |
|
||||||
|
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` | — | optional; blank → contact form uses `mailto:` |
|
||||||
|
| `CONTACT_TO` | `UOMysticmoon@gmail.com` | contact recipient |
|
||||||
|
| `CLIENT_ORIGIN` | `http://localhost:5173` | enables CORS in dev only |
|
||||||
|
| `LOG_LEVEL` / `FILE_LOG_LEVEL` | `info` / `debug` | console / file verbosity |
|
||||||
|
| `LOG_TO_FILE` / `LOG_DIR` / `LOG_FILE` | `true` / `<server>/logs` / `app.log` | log file (bind-mounted to `./logs` in Docker) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
JWT in an httpOnly, `SameSite=Lax` cookie (`Secure` auto-detected) · bcrypt hashing · login &
|
||||||
|
contact rate limiting · `express-validator` on writes · `helmet` · admin routes `noindex` +
|
||||||
|
`robots.txt` disallow · `trust proxy` for correct client IPs behind Pangolin · first admin seeded
|
||||||
|
from env (no hardcoded credentials) · `.env` git-ignored. Passwords and request bodies are never
|
||||||
|
logged. SMTP is optional — the contact form falls back to a `mailto:` link when unconfigured.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
@@ -71,18 +250,18 @@ Every log line goes to **both the console and a log file**, timestamped and leve
|
|||||||
2026-06-26T18:55:20.110Z ERROR [error] GET /api/v1/public/wiki -> 500 ... {"stack":"..."}
|
2026-06-26T18:55:20.110Z ERROR [error] GET /api/v1/public/wiki -> 500 ... {"stack":"..."}
|
||||||
```
|
```
|
||||||
|
|
||||||
What's captured: startup config banner, schema/seed steps, **HTTP access logs** (real client
|
Captured: startup config banner, schema/seed steps, **HTTP access logs** (real client IP via
|
||||||
IP via `trust proxy`, the authenticated admin, method/URL/status/time/size), login
|
`trust proxy`, the authenticated admin, method/URL/status/time/size), login success/failure,
|
||||||
success/failure, rate-limit hits, site-mode changes, all errors with stack traces, and
|
rate-limit hits, site-mode changes, all errors with stack traces, and graceful shutdown. Console
|
||||||
graceful shutdown. Passwords and request bodies are never logged.
|
verbosity is `LOG_LEVEL`; the file keeps the fuller `FILE_LOG_LEVEL` record. In Docker the file is
|
||||||
|
bind-mounted to `./logs/app.log` and `docker compose logs -f app` shows the console stream.
|
||||||
|
|
||||||
| Env | Default | Meaning |
|
---
|
||||||
|---|---|---|
|
|
||||||
| `LOG_LEVEL` | `info` | console verbosity |
|
|
||||||
| `FILE_LOG_LEVEL` | `debug` | file verbosity (keeps a full record) |
|
|
||||||
| `LOG_TO_FILE` | `true` | set `false` for console-only |
|
|
||||||
| `LOG_DIR` | `<server>/logs` (`/app/logs` in Docker) | log directory |
|
|
||||||
| `LOG_FILE` | `app.log` | log file name |
|
|
||||||
|
|
||||||
In Docker the log file is bind-mounted to `./logs/app.log` on the host; `docker compose logs -f app`
|
## Deployment behind Pangolin
|
||||||
also shows the console stream.
|
|
||||||
|
`docker compose up -d --build` exposes the `app` container on `0.0.0.0:3000` (no `127.0.0.1`
|
||||||
|
binding) so Pangolin can reach it. Point a Pangolin resource at `app:3000`. Because `COOKIE_SECURE`
|
||||||
|
defaults to `auto`, the admin login works both directly via the LAN IP over HTTP **and** through
|
||||||
|
Pangolin over HTTPS — no config change needed. MariaDB stays on the private Compose network
|
||||||
|
(no published port by default); data persists in the `dbdata` volume, uploads in `uploads`.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': { target: 'http://localhost:3000', changeOrigin: true },
|
'/api': { target: 'http://127.0.0.1:3000', changeOrigin: true },
|
||||||
'/uploads': { target: 'http://localhost:3000', changeOrigin: true },
|
'/uploads': { target: 'http://127.0.0.1:3000', changeOrigin: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
$base = 'http://127.0.0.1:3000'
|
|
||||||
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
|
|
||||||
|
|
||||||
function Req($method, $path, $bodyObj) {
|
|
||||||
$p = @{ Method = $method; Uri = "$base$path"; UseBasicParsing = $true; WebSession = $session; ErrorAction = 'Stop' }
|
|
||||||
if ($null -ne $bodyObj) { $p.Body = ($bodyObj | ConvertTo-Json -Depth 6 -Compress); $p.ContentType = 'application/json' }
|
|
||||||
Invoke-WebRequest @p
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Output 'waiting for health...'
|
|
||||||
$up = $false
|
|
||||||
for ($i = 0; $i -lt 40; $i++) {
|
|
||||||
try { if ((Invoke-WebRequest "$base/api/health" -UseBasicParsing).StatusCode -eq 200) { $up = $true; break } } catch {}
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
}
|
|
||||||
if (-not $up) { Write-Output 'server never came up'; return }
|
|
||||||
|
|
||||||
Req POST '/api/v1/auth/login' @{ username = 'admin'; password = 'adminpass123' } | Out-Null
|
|
||||||
Req PUT '/api/v1/admin/site-mode' @{ mode = 'live' } | Out-Null
|
|
||||||
|
|
||||||
# News
|
|
||||||
Req POST '/api/v1/admin/posts' @{ category='news'; title='The world map enters closed testing'; excerpt='The Mysticmoon overworld is feature-complete enough for a small group to wander.'; body='<p>Travel between the three starting towns is live, moongates are seeded, and the first dungeon level is open for stress-testing.</p>'; published=$true } | Out-Null
|
|
||||||
Req POST '/api/v1/admin/posts' @{ category='news'; title='Crafting trees and resource gathering'; excerpt='Mining, lumberjacking, and the first tier of smithing and tailoring are in.'; body='<p>Resource respawn timers are tuned for a small population.</p>'; published=$true } | Out-Null
|
|
||||||
|
|
||||||
# Five on Friday
|
|
||||||
Req POST '/api/v1/admin/posts' @{ category='five-on-friday'; title='Five on Friday #07'; body='<ol><li>Moongates now route correctly between all three regions.</li><li>The blacksmith UI got a readability pass.</li><li>Something large now lurks in the Hollow Deeps.</li><li>Next week: player housing placement rules.</li><li>The full-moon lighting in town looks lovely.</li></ol>'; published=$true } | Out-Null
|
|
||||||
|
|
||||||
# Newsletter
|
|
||||||
Req POST '/api/v1/admin/posts' @{ category='newsletter'; slug='june-2026'; title='A world you can walk across'; excerpt='The longest month of building yet - here is everything that landed.'; body='<p>June was the month Mysticmoon stopped being a set of disconnected systems and started feeling like a place.</p><h2>The overworld opens</h2><p>For the first time, testers walked the road from Mistholme to the eastern moongate.</p><h2>Crafting takes root</h2><p>Mining, lumberjacking, smithing, and tailoring all came online.</p>'; published=$true } | Out-Null
|
|
||||||
|
|
||||||
# Screenshot (uses the hero image already served at /assets)
|
|
||||||
Req POST '/api/v1/admin/posts' @{ category='screenshots'; title='Mistholme at dusk'; excerpt='The square at Mistholme, lanterns lit at dusk.'; image_url='/assets/img/uomysticmoon-main-hero.png'; published=$true } | Out-Null
|
|
||||||
|
|
||||||
# Wiki body with H2 sections so the article TOC renders
|
|
||||||
Req PUT '/api/v1/admin/wiki/new-player-guide' @{ title='New Player Guide'; body='<p>Everything you need to find your feet in your first hour on Mysticmoon.</p><h2>When you arrive</h2><p>New characters wake in <strong>Mistholme</strong>, the central starting town.</p><h2>Choosing your first skills</h2><p>You do not pick a class - you grow into one by using skills.</p><h2>Staying alive</h2><ul><li>Towns are safe. The wilderness is not.</li><li>Bank your gold and reagents often.</li><li>Keep bandages and a spare weapon.</li></ul>' } | Out-Null
|
|
||||||
|
|
||||||
Write-Output 'SETUP DONE - site is live with sample content'
|
|
||||||
Reference in New Issue
Block a user