# Mathew — a wiki for mathematics A collaborative wiki of mathematical concepts. Articles are written in **Markdown** with **LaTeX** (KaTeX), stored in MongoDB behind an Express API, and rendered by a Nuxt 4 app as an open book with a table of contents, a table of topics, a link graph, and a review-then-publish flow for edits. The interface speaks five languages — English, Spanish, Catalan, French, German — chosen per account, and articles themselves are multilingual: one article can exist in several languages, each with its own address. ``` mathew/ ├── dev.sh # start/stop both dev servers together ├── docker-compose.yml # nginx + frontend + backend (prebuilt images) ├── nginx.conf # reverse proxy: / → frontend, /api/ → backend ├── backend/ # Express 5 + Mongoose API (port 5000) └── frontend/ # Nuxt 4 / Vue 3 wiki UI (port 3000) ``` ## Quick start Requires **MongoDB** on `localhost:27017` (database `mathew`) and **Node 20+**. ```bash # 1. API cd backend npm install cp .env.example .env # then set JWT_SECRET npm run seed # optional: sample articles with LaTeX npm run dev # → http://localhost:5000 # 2. UI cd ../frontend npm install npm run dev # → http://localhost:3000 ``` Or run both together from the repo root: ```bash ./dev.sh # both dev servers in the foreground (Ctrl+C stops both) ./dev.sh start|stop|restart|status ./dev.sh logs [all|backend|frontend] ``` ### Scripts | Where | Command | What it does | | --- | --- | --- | | root | `./dev.sh [dev]` | Both dev servers, foreground, tagged output, one Ctrl+C stops both | | `backend/` | `npm run dev` | API with `node --watch` (no build step) | | `backend/` | `npm start` | API without watch | | `backend/` | `npm run seed` | A handful of sample articles | | `backend/` | `npm run seed:bulk` | 100 cross-linked articles for load-testing the UI and graph (idempotent) | | `frontend/` | `npm run dev` / `build` / `preview` | Nuxt dev server, production build, preview of the build | There is **no test suite, linter, or typecheck script** configured. ### Configuration | Where | Variable | Default | Meaning | | --- | --- | --- | --- | | `backend/.env` | `PORT` | `5000` | API port | | `backend/.env` | `MONGO_URI` | `mongodb://127.0.0.1:27017/mathew` | Mongo connection | | `backend/.env` | `JWT_SECRET` | *(insecure dev key)* | Signs session tokens — set this in production | | `backend/.env` | `AUTH_TOKEN_TTL` | `7d` | How long a session lasts | | `frontend` env | `NUXT_PUBLIC_API_BASE` | `http://localhost:5000` | Where the UI calls the API | ## How it works **Reading is open; writing takes an account.** Anyone can register, and the **first account ever created becomes the admin** (on a database whose accounts all predate roles, the oldest one gets it at startup). The admin hands the role on or takes it back from `/admin`; nobody may change their own role, so the dashboard always has a door. - An **admin**'s save applies immediately. - A **member**'s save is answered `202` and filed as an **edit proposal** instead — the live article does not move. Saving again while a proposal waits *revises* it, and each proposal keeps a snapshot of the article as it read when the request was sent, so the desk can show a diff and warn when the page has moved since. - Proposals include **new articles**: a request for a page the wiki does not have yet keeps the address it asks for and appears only when approved. - Approving or rejecting happens at `/admin/proposals`, and approval runs the same code an admin's direct write runs — so a retitle re-slugs, and a language the article already has in another version is refused. - **Deleting is admin-only**, and it settles that article's waiting proposals with it. Articles name no author, so they outlive the account that wrote them. ### Languages Two language ideas, deliberately apart: - The **interface language** (`en`, `es`, `ca`, `fr`, `de`) is a preference and never part of a URL. Precedence: the signed-in account's choice → the `mathew-locale` cookie → the browser → English. Pick it in the account popup. - The **article language** appears in the address: `/wiki//`. Every version of an article has its own address, English included — `/wiki/en/bayes-theorem` and `/wiki/fr/bayes-theorem` are one article two ways. The short form `/wiki/` redirects onto the version the reader should see: their own language if the article speaks it, English failing that. Switching an article's language turns the page of the open book rather than opening a second one, and "recently viewed" remembers article + language as a single entry. ### Themes Light and dark, plus eight accent colours, chosen in the settings popup and applied before first paint so there is no flash. Colours are CSS custom properties swapped by `data-theme` / `data-accent` on ``. ## Pages | Route | What it is | | --- | --- | | `/` | Search, recently viewed, a way into the shelf | | `/articles` | The shelf, filed by topic — one folder per topic, opening onto a batch-at-a-time grid of cards; language and topic filters are applied by the API | | `/graph` | The wiki's link map: one node per article, one edge per linked pair | | `/wiki//` | One language version of an article, drawn as a page of the book with its table of contents | | `/wiki/` | The short form, naming no language — turned onto the version the slug speaks | | `/wiki//edit` | Markdown editor with live preview (Write / Split / Preview) | | `/new` | Create a new article | | `/admin` | Accounts and roles (admin only) | | `/admin/proposals` | The edit-request desk: diff, approve, reject (admin only) | ## Writing articles GitHub-ish Markdown — headings, tables, lists, blockquotes, fenced code with syntax highlighting — plus LaTeX: `$E = mc^2$` inline, `$$ … $$` in display mode. Number-set macros `\R`, `\N`, `\Z`, `\Q`, `\C` are available. Raw HTML in an article is escaped, and everything rendered is sanitized with DOMPurify. Link between articles with wiki links — `[[Bayes' theorem]]`, `[[Fourier series#definition]]`, `[[Zeno's paradoxes|Zeno]]` — or with a normal Markdown link to `/wiki/` or `/wiki//`. Both feed the graph view. Shortcuts: `/` focuses search on the home page, `Ctrl/Cmd+K` opens the search popup, `Ctrl/Cmd+S` saves in the editor. Changing an article's title can change its slug — old links then 404, as no redirects are kept. ## API `GET /health` answers `{ status, mongo }`. Everything below lives under `/api`. Reads are public; writes need `Authorization: Bearer ` from `/auth/login` or `/auth/register`. All errors answer `{ message }`. ### Accounts | Method | Endpoint | Notes | | --- | --- | --- | | POST | `/auth/register` | Creates the account and returns its session; the first account ever is an admin | | POST | `/auth/login` | A wrong name and a wrong password answer the same way | | GET | `/auth/me` | Who a token belongs to | | PUT | `/auth/me` | Rename | | PUT | `/auth/password` | Needs the current password | | PUT | `/auth/locale` | Interface language — one of `en es ca fr de` | | PUT | `/auth/avatar` | A small data URL; `""` clears it | ### Articles Articles are addressed by **slug**, not by Mongo id. | Method | Endpoint | Notes | | --- | --- | --- | | GET | `/articles` | `{ articles, total, hasMore }`, paged **by article** — `?q=`, `?tag=`, `?lang=`, `?limit=`, `?offset=`; `?prefer=` picks which version stands for an article, `?preferFirst` ranks those versions first; no `limit` means the whole shelf | | GET | `/articles/facets` | `{ total, topics, languages }` counted over the whole shelf | | GET | `/articles/random` | One random article | | GET | `/articles/graph` | `{ nodes, edges }` — the link map built from wiki and Markdown links | | GET | `/articles/slug/:slug` | One article | | POST | `/articles` | Create (`title` required; the slug is generated and uniquified). Admin applies; a member is answered `202 { proposal }` | | PUT | `/articles/slug/:slug` | Update. Admin applies; a member is answered `202 { proposal }` | | DELETE | `/articles/slug/:slug` | Admin only | ### Administration Everything behind `requireAuth, requireAdmin`. | Method | Endpoint | Notes | | --- | --- | --- | | GET | `/admin/users` | Every account, newest first | | PUT | `/admin/users/:id/role` | `role` is `admin` or `member`; never your own | | DELETE | `/admin/users/:id` | Close an account — never your own; its articles stay | | GET | `/admin/proposals` | Every request, decided ones included, with its diff material | | GET | `/admin/proposals/count` | Just `{ pending }` — what the sidebar's badge wears | | PUT | `/admin/proposals/:id/approve` | Writes the change; a conflict refuses it and the request keeps waiting | | PUT | `/admin/proposals/:id/reject` | Refuses; the page stays put | `/api/items` is a small demo resource (CRUD over one schema) left in as a reference for how a router, model and error path fit together. ## Deploy `docker-compose.yml` runs nginx in front of prebuilt frontend and backend images: nginx listens on `3000` and routes `/api/` to the backend and everything else to the Nuxt server, passing the standard forwarded headers. Point `NUXT_PUBLIC_API_BASE` and `MONGO_URI` at the deployment's real addresses and set a real `JWT_SECRET` — the fallback key is a development convenience only, and anyone holding it can mint a session. ## Notes for contributors [`AGENTS.md`](AGENTS.md) is the working map of the codebase: file-by-file layout, the backend and frontend conventions, and the gotchas that are easy to get wrong (article identity vs. document slug, versioned addresses, MongoDB's reserved `language` field, localization rules). ## License MIT.