Pages and routes — every URL the app serves
Every URL Swiftpoint Hub answers on, who it is for, and what it needs to serve it. This is a route inventory: which path returns which document, image or JSON,
Every URL Swiftpoint Hub answers on, who it is for, and what it needs to serve it. This is a route inventory: which path returns which document, image or JSON, and which gate stands in front of it. Request and response bodies belong to the API reference; the content of each page belongs to the feature chapters such as the Profile Builder, the OLED editor and the Guides system.
Base path
Every path below is relative to BASE_PATH (env var; /swiftpoint in the deployed example). API routers are mounted at ${BASE_PATH}/api/... in server.js:140-147; the docs router, the SEO router and express.static are mounted at BASE_PATH || '/' in server.js:163-166. Order matters: the API routers are consulted first, then /docs/*, then the page routes, then static files. Nothing is registered after express.static, so a static file can never shadow a route.
The meaningful paths live in lib/paths.js (BASE_PATH, GALLERY_PATH = /profiles/hub, galleryPath()), because the sitemap, the header, the footer, the admin redirect and the client scripts all have to agree on where the gallery is.
Navigation
The header (lib/header.js) is generated from one NAV array and injected into every page. Six items:
| Item | Href | What it opens | Marked active on |
|---|---|---|---|
| Browse | ./profiles/hub |
Community gallery | / exactly, /profiles/…, /users/… |
| Profile Builder | ./editor |
Profile Builder | /editor |
| OLED Editor | ./oled |
OLED editor | /oled |
| Checker | ./check |
.spcf checker | /check |
| Guides | ./docs |
Guides | /docs |
| Forum | ./forum |
Forum and tickets | /forum, /updates |
Each item carries a data-paths attribute; public/js/chrome.js derives the active item from it using longest-prefix matching, with / treated as exact-only and a trailing slash as a pure prefix. Browse is active on a profile page because those pages sit inside Browse; Forum is active on the updates list because both are the same destination. /admin and /records are deliberately absent from NAV, from the footer and from the sitemap, and test/header.test.js asserts that — a site-administration page cannot quietly become a nav item.
Alongside the nav the header carries a Support link (external, SUPPORT_URL), a notification bell with panel and "Mark all read", an #admin-btn that stays hidden until /api/auth/me reports admin, and a login/upload/user-chip cluster with a user menu (Your profile, Ticket queue, Write an update, Admin panel, Log out). See Roles and permissions.
Page routes
These are static HTML files in public/, read at request time and re-injected with SEO meta and JSON-LD by routes/seo.js (or rendered by routes/docs.js for the guides). Each route rewrites <meta charset="UTF-8" /> to add <base href="{SITE_URL}/">, replaces the <!--SEO:META--> marker, drops the template's generic <meta name="description"> so there is only one, and substitutes the title.
| Route | Source file | Audience | Gate |
|---|---|---|---|
/ |
public/index.html |
Anyone | none |
/profiles/hub |
public/gallery.html |
Anyone | none |
/profiles/:id |
public/profile-beta.html |
Anyone | none |
/users/:id |
public/user.html |
Anyone | none |
/editor |
public/editor.html |
Anyone | none |
/oled |
public/oled.html |
Anyone | none |
/check |
public/check.html |
Anyone | none |
/records |
public/records.html |
Staff | requireAdminPage |
/admin |
public/admin.html |
Admins | requireAdminPage |
/docs, /docs/<slug> |
server-rendered | Anyone | none |
/forum |
public/forum.html |
Anyone | none |
/forum/t/:id |
public/thread.html |
Anyone | none (404 if the thread is gone) |
/updates |
public/updates.html |
Anyone | none |
/privacy, /imprint |
generated by lib/legal.js |
Anyone | none |
/ — front page
For a reader deciding what the site is. Explains the Z3 mouse and the four tools. Server-rendered rather than script-filled, because the front page is what a crawler and a reader without JavaScript see.
- Four
data-statcounters (profiles,games,mappings,members) are rewritten server-side from SQL, so a number is never only present after a script runs. - Six newest profiles, 4 published guides (read from the guides engine, linking into
/editor?record=…), 3 newest updates and 3 open tickets are rendered into<ul>placeholders. Imported profiles are badgedimportedrather than being passed off as new uploads. - Meta: canonical
/,WebSiteJSON-LD, og/twittersummary_large_imagewith/og-image.pngdeclared at 1200×630. The description stays in the template's own markup; this route does not inject a second one.
/profiles/hub — community gallery
GALLERY_PATH from lib/paths.js. Registered above /profiles/:id, and it has to be: the single-segment handler answers for anything after /profiles/, so without it the word hub would be read as a profile id and 404. Search, filter by tag or game, sort, upload and pagination are drawn by public/js/app.js. ItemList JSON-LD lists the 20 newest profiles with URL, name, description and author — the honest summary of a list the crawler cannot see, built from the same rows the gallery shows and no more. See Tools gallery and profiles.
/profiles/:id — profile detail
One profile as a full page: button mappings, deep-click stages, nested bindings, DPI and OLED frames, plus comments and voting. There is no macros section — PredecessorId links are drawn as a Nested bindings tree.
- The legacy
/57bform normalises to id 57 in-route (routes/seo.js:910); a non-numeric id is a 404. viewsis incremented, and a pageview recorded, for every reader except an admin session (isAdminSession).CreativeWorkJSON-LD,og:typearticle, og:image the per-profile/profiles/:id/share.png— the share card shows the actual bindings rather than a logo.- The template ships as
noindex,followbecause it was the beta preview; the route rewrites it toindex,follow, since the default layout is the public one.
/users/:id — member page
Public member view: bio, stats, profiles, recent comments and threads. ProfilePage JSON-LD, og:type profile, og:image /og-image/user-<id>.png. It also flips the template's noindex,follow to index,follow. A non-numeric or unknown id gives the 404 page.
/editor — Profile Builder
The .spcf designer. noindex,nofollow, WebApplication JSON-LD with applicationCategory: DesignApplication, og:image /og-image/tool-editor.png. See Profile Builder.
The route injects body[data-can-record="1"|"0"], and the injection is attribute-aware (setHtmlAttr) rather than a literal '<body>' replace: the editor's body is <body data-app-shell>, so a literal replace silently no-ops and the recorder — which gates on document.body.dataset.canRecord — exits before revealing its button, which reads to an admin as a removed feature with nothing anywhere reporting a failure.
/oled — OLED editor
The 64×32 monochrome frame editor. WebApplication / DesignApplication, og:image /og-image/tool-oled.png. See OLED editor.
/check — .spcf checker
Opens a .spcf entirely in the browser; nothing is uploaded or stored. noindex,nofollow, WebApplication with applicationCategory: UtilityApplication. The page loads /js/spcfParser.js, the same UMD file the server parses with. See .spcf checker.
/records — recordings manager
Admin-only authoring surface for guide walkthroughs. noindex,nofollow, og:image /og-image/tool-records.png. Behind requireAdminPage like /admin: recordings are authored content, and asking for this page must never land the visitor somewhere unrelated with no explanation. See Walkthrough recorder.
/admin — admin panel
Users, profiles, comments, reports, staff, settings and the guides editor. Behind requireAdminPage. Only <base> is injected; the panel is a signed-in application, not a landing page. See Admin panel.
The admin page gate
requireAdminPage(req, res, pathname, denied) in routes/seo.js:534:
| Session state | Response |
|---|---|
| Signed out | 302 to {SITE_URL}/api/auth/discord/login?returnTo={BASE_PATH}{pathname} |
| Signed in, not admin | 302 to {BASE_PATH}/profiles/hub?admin=denied |
| Signed in, admin | the page itself |
The gallery reads ?admin=denied and explains it (public/js/app.js); the front page has no such handler, and a silent jump home is what this gate was written against. returnTo is a full browser path including the base prefix — the OAuth callback prepends only the origin. The same rule breaks the login loop: a non-admin who asked for /admin comes back from Discord to /profiles/hub?admin=denied instead of being sent to /admin again (routes/auth.js:176).
/forum, /forum/t/:id, /updates
All three go through serveShell (routes/seo.js:1030), which injects <base> and body[data-base] — the app's own answer to "am I mounted under a prefix", where guessing from the first path segment gets it wrong at the root: on /forum the client would read the site as living under /forum and prefix every API call one segment too deep.
/forum— forum index: categories, latest activity, ticket queue. Pageview tracked for non-admins./forum/t/:id— one thread or ticket. The title carries the status label;DiscussionForumPostingJSON-LD with reply and view counters. A non-numeric or unknown id is the shared 404 page, because a deleted thread must not answer with an empty shell./updates— changelog, split into website updates and mouse updates.
See Forum and tickets and Updates.
/docs, /docs/<slug>
Server-rendered by routes/docs.js; see Docs routes. See Guides system.
/privacy, /imprint
Rendered from lib/legal.js rather than from files, so the claims cannot drift from the implementation — test/legal-pages.test.js fails when a statement stops matching the code. Both carry the standard header, footer, theme script and auth script, so the header on these pages is live rather than a picture.
Docs routes
routes/docs.js, mounted before the SEO router. All pages are server-rendered HTML with no inline scripts (CSP is script-src 'self').
| Method | Path | Purpose | Notes |
|---|---|---|---|
| GET | /docs, /docs/ |
README index | Renders st.flat[0]; docs 404 shell if there is no guide at all |
| GET | /docs/og/<slug>.png |
Per-guide share card | 1200×630 from lib/docs-og.js; registered before the generic image matcher; Cache-Control: public, max-age=3600 |
| GET | /docs/recordings/<slug>.rec.json |
A recording referenced by a ::: steps block |
The one recordings directory; repo file first via eng.resolveRec, then the recordings table |
| GET | /docs/<file> (images only) |
Local image referenced from a guide | .png .jpg .jpeg .webp .gif .svg .avif, MIME from extension |
| GET | /docs/<slug> |
One guide | Tracks a pageview; 301 to the canonical spelling when the engine matched case-insensitively, query string preserved |
| GET | /api/docs-tree |
Flat metadata list | Used by search |
| GET | /api/guides |
Published walkthroughs | Title, recording URL, owning guide, step count; no file paths, no author ids |
| GET | /api/docs-graph |
Nodes and edges for the notes map | |
| GET | /api/docs/<slug>/raw |
Original markdown | text/plain |
| GET | /api/docs/<slug>/discord |
Discord-ready text export | text/plain |
Details worth knowing:
- Matcher order is load-bearing:
/docs/og/...pngis registered before the generic image matcher so a per-guide card is never served as a guide asset. - The
.rec.jsoncapture is non-greedy and keeps the suffix soresolveReccan validate it. A recording saved to the hub but never published as a repo file still resolves, which is why the DB fallback exists. - One guide, one URL.
/docs/profile-builder/Testingand/docs/profile-builder/testingresolve to the same file, so the non-canonical spelling is a 301 with the query string carried over. A folder address (/docs/profile-builder) is not a redirect — it is that folder's own page. - A miss anywhere under
/docs/*rendersrender404(): the full docs shell with tree and nav,noindex,nofollow, and "No such guide".
SEO routes
routes/seo.js.
| Method | Path | Purpose | Cache |
|---|---|---|---|
| GET | /robots.txt |
Crawler policy, text/plain |
— |
| GET | /sitemap.xml |
URL set, application/xml |
— |
| GET | /og-image.png |
Home card: the site's numbers and what each of the four tools does | public, max-age=86400 |
| GET | /og-image/<variant>.png |
tool-editor, tool-oled, tool-check, tool-records, user-<digits>, gallery |
public, max-age=86400 |
| GET | /profiles/:id/share.png |
Per-profile card showing its bindings | public, max-age=3600 |
| GET | /js/spcfParser.js |
UMD .spcf parser from lib/ |
public, max-age=3600 |
| GET | /js/spcfWriter.js |
UMD .spcf writer from lib/ |
public, max-age=3600 |
| GET | /js/step-diff.js |
UMD semantic diff from lib/ |
public, max-age=3600 |
Details worth knowing:
robots.txtmust be plain text. It used to be answered by the API's catch-all 404 withContent-Type: application/json, which a crawler reads as "no robots here"; and a crawler that reads/robots.txtat the host root and a narrower one at/swiftpoint/takes the narrower, so an inconsistent file is a policy that does not apply. Content:User-agent: *,Allow: /,Disallow: /api/,Disallow: /admin,Disallow: /records,Sitemap: {SITE_URL}/sitemap.xml.sitemap.xmlentries:/daily 1.0;/profiles/hubdaily 0.9;/docsweekly 0.5; every guide except README monthly 0.7; every profile weekly 0.8 withlastmod; every user weekly 0.4 withlastmod;/forumdaily 0.7;/updatesweekly 0.6; the 100 most recently active threads weekly 0.5 withlastmod. A sitemap is not an archive, which is why threads are capped.- An unknown og-image variant falls back to the home card rather than 404ing: a share preview showing the site's own card beats one showing nothing, and the URLs are generated from the same list the renderer serves, so this is a guard rather than a path anyone takes.
/profiles/:id/share.pnganswers404 text/plain Not foundfor a non-numeric id or a render failure.- The three
/js/*.jsroutes serve files fromlib/, notpublic/js/, so the browser parses a.spcfwith exactly the code the server parses it with.routes/seo.jsis mounted beforeexpress.static, and no file of the same name exists inpublic/js/.
See SEO and analytics.
API routes
All JSON unless noted. Auth gates come from routes/auth.js: requireUser → 401 {"error":"Please log in with Discord to do that."}, requireModerator → 403 {"error":"Moderators only."}, requireAdmin → 403 {"error":"Admins only."}. Both role flags are recomputed from the Discord-ID allowlist on every call and never read from the session snapshot, so a session that predates a role change cannot hide a panel from somebody who has it.
Request and response shapes are in the API reference; the tables below are the inventory.
/api/auth (4)
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /discord/login |
none | 302 to Discord OAuth; state stored in session, returnTo sanitised |
| GET | /discord/callback |
none | Exchange code, upsert user, regenerate session, redirect |
| POST | /logout |
none | Destroy the session |
| GET | /me |
none | Always 200; identity plus recomputed role flags |
returnTo sanitisation (safeReturnTo): must start /, must not start //, must not contain a backslash, must contain no control characters — a control character in a Location header is a response-splitting primitive, so the value is refused rather than sanitised. /discord/login answers 503 when DISCORD_CLIENT_ID or DISCORD_CLIENT_SECRET is unset; /discord/callback answers 400 with a short HTML page on any failure. See Auth and roles.
/api/profiles (14)
| Method | Path | Auth | Purpose | Limits |
|---|---|---|---|---|
| GET | / |
none | List, filter, sort | q, tag, game, sort ∈ newest, oldest, name, buttons, dpi, votes, visited, downloaded |
| GET | /tags |
none | Tag list with counts | |
| GET | /stats |
none | {profiles, games, mappings} |
|
| GET | /games |
none | Games present in the table | |
| GET | /:id |
none | One profile with its parsed bindings | |
| GET | /:id/download |
none | The .spcf, stored 1:1, download counted |
|
| POST | /:id/vote |
user | value 1, -1 or 0 |
Cannot vote on your own profile |
| POST | / |
user | Multipart upload | 429 at the per-user cap; admins exempt |
| PATCH | /:id |
owner or admin | Metadata only | |
| PUT | /:id |
owner or admin | Multipart; replaces the file in place | |
| GET | /:id/comments |
none | Comment thread | |
| POST | /:id/comments |
user | Add a comment | kind: 'patch' only for the profile author |
| DELETE | /:id/comments/:cid |
comment author or admin | Remove one comment | |
| DELETE | /:id |
uploader or admin | Delete a profile |
Limits in routes/profiles.js: file 2 MB via multer (LIMIT_FILE_SIZE → 413), MAX_TITLE = 80, MAX_AUTHOR = 60, MAX_DESCRIPTION = 400, MAX_COMMENT = 1000, per-user cap default 3 (MAX_PROFILES_PER_USER, overridable at runtime by the max_profiles_per_user setting). The public profile payload deliberately carries no Discord id: ownership is the server's answer to give, not the browser's, and exposing it hands an anonymous visitor a stable identifier for every person on the site.
/api/notifications (4, all user)
| Method | Path | Auth | Purpose | Limits |
|---|---|---|---|---|
| GET | / |
user | Feed, unread first | 50 |
| GET | /unread-count |
user | {unread} |
|
| GET | /stream |
user | Server-sent events | text/event-stream, ping every 25 s, X-Accel-Buffering: no |
| POST | /read |
user | {all: true} or {ids: […]} |
ids integer-filtered, de-duplicated, capped at 100 |
/api/reports (3)
| Method | Path | Auth | Purpose | Notes |
|---|---|---|---|---|
| POST | / |
user | Report a profile, comment or post |
One open report per user per target → 409. A post notifies moderators with threadId + postId so the exact reply can be opened; a profile or comment goes to admins. Reason capped at 500. |
| GET | / |
admin | The queue | status ∈ open, resolved, dismissed, all; anything invalid falls back to open |
| POST | /:id/status |
admin | resolved or dismissed |
400 if the report is no longer open |
/api/admin (8 routes, all admin)
| Method | Path | Auth | Purpose | Notes |
|---|---|---|---|---|
| GET | /overview |
admin | One payload for the whole panel | Users, profiles, the 100 newest comments, reports, the admin's own activity, settings, and the traffic block |
| GET | /settings |
admin | Current runtime limits | |
| POST | /settings |
admin | Update a known limit | max_profiles_per_user must be an integer 1–100 |
| GET | /staff |
admin | Staff list plus knownPeople |
|
| POST | /staff |
admin | Add a moderator or admin | By Discord ID, or by username resolved against accounts that have signed in here; more than one match → 400 |
| PATCH | /staff/:discordId |
admin | Promote or demote | |
| DELETE | /staff/:discordId |
admin | Remove somebody | |
| POST | /profiles/:id/transfer |
admin | Reassign a profile to another user | Notifies both sides |
/api/admin/docs (9 routes, all admin)
Mounted from routes/docs-write.js inside routes/admin.js, so everything under /api/admin is one surface with one gate — and so the check in test/admin-access.test.js that every admin route sits behind requireAdmin can actually see these routes. The gate is on each route individually rather than inherited: this writes files, and one endpoint in particular deletes them.
| Method | Path | Auth | Purpose | Notes |
|---|---|---|---|---|
| GET | /api/admin/docs |
admin | Tree plus what a new name would collide with, plus the limits | |
| GET | /api/admin/docs/:slug(.*) |
admin | One guide's raw content | |
| POST | /api/admin/docs/folders |
admin | Create a folder | |
| POST | /api/admin/docs/documents |
admin | Create a document | |
| PUT | /api/admin/docs/:slug(.*) |
admin | Save content | |
| PATCH | /api/admin/docs/:slug(.*) |
admin | Rename | Returns linkedGuides so the editor can show what the rename breaks |
| PATCH | /api/admin/docs/folders/:slug(.*) |
admin | Rename or move a folder | |
| DELETE | /api/admin/docs/folders/:slug(.*) |
admin | Two-step delete | First call 409 with needsConfirm and counts; second confirms |
| DELETE | /api/admin/docs/:slug(.*) |
admin | Delete a guide | |
| POST | /api/admin/docs/preview |
admin | Render markdown as the guide will appear |
Preview renders with the GUIDES engine, not the forum's, so wikilinks, relative images and ::: steps resolve against the slug the guide will have; previewing with the wrong renderer shows a link that 404s the moment it is published, which is the one thing a preview exists to prevent. It accepts content or body and answers 400 Nothing to preview. otherwise. Limits in lib/docs/write.js: MAX_NAME = 60, MAX_TITLE = 140, MAX_DESCRIPTION = 300, MAX_BODY = 200000, MAX_TAGS = 12; routes/docs-write.js additionally caps the body at 220 000 so an oversized body is refused before it is parsed. The panel editor caps at 32 000.
/api/users (2)
| Method | Path | Auth | Purpose | Limits |
|---|---|---|---|---|
| GET | /:id |
none | Public member payload | Bio ≤ 300, comments ≤ 100, threads ≤ 10; the Discord id is deliberately absent |
| POST | /:id/bio |
own account | Edit the bio | 403 for anybody else |
/api/forum (25)
| Method | Path | Auth | Purpose | Notes |
|---|---|---|---|---|
| GET | / |
none | Forum root payload | |
| GET | /categories |
none | Categories | |
| POST | /categories |
mod | Create a category | Name ≤ 60, description ≤ 300 |
| PATCH | /categories/:id |
mod | Rename or reorder | |
| DELETE | /categories/:id |
mod | Remove a category | |
| GET | /threads |
none | Thread list | category, status, q, sort ∈ activity, newest, oldest, replies, views, limit, offset |
| POST | /threads |
user | Start a thread | The first post is the body; the author is subscribed; a ticket notifies moderators. Title 4–120, body 2–8000 |
| GET | /threads/:id |
none | Thread, posts, and the ticket events trail |
offset, around, limit default 50 max 200. A view is counted only for signed-in readers |
| POST | /threads/:id/posts |
user | Reply | |
| POST | /posts/:id/edit |
author or staff | Edit a post | |
| DELETE | /posts/:id |
mod | Tombstone, not a hard delete | |
| POST | /threads/:id/pin |
mod | Pin | |
| POST | /threads/:id/lock |
mod | Lock | |
| POST | /threads/:id/move |
mod | Move to another category | |
| POST | /threads/:id/status |
mod | Ticket status | Note ≤ 300 |
| DELETE | /threads/:id |
mod | Delete a thread | |
| GET | /tickets |
mod | Ticket queue | |
| POST | /preview |
user | Render a post's markdown | Capped at 32 000 (max(forum.MAX_BODY, updates.MAX_BODY)) |
| POST | /threads/:id/subscribe |
user | Subscribe | |
| DELETE | /threads/:id/subscribe |
user | Unsubscribe | |
| GET | /search |
none | Threads and posts | q cleaned to 80 chars; up to 20 threads + 20 posts |
| GET | /updates |
none | Changelog data, kinds, accents, tags, counts | kind, q, limit, offset |
| POST | /updates |
mod | Write an update | |
| PUT | /updates/:id |
mod | Edit an update | |
| DELETE | /updates/:id |
mod | Delete an update |
Limits in lib/forum.js: MAX_TITLE 120, MIN_TITLE 4, MAX_BODY 8000, MIN_BODY 2, MAX_STATUS_NOTE 300, MAX_CATEGORY_NAME 60, MAX_CATEGORY_DESCRIPTION 300, DEFAULT_LIMIT 50, MAX_LIMIT 200.
There is deliberately no route to edit a thread title — only posts can be edited. A thread's title is its identity in a chat window, a bookmark and a search result; editing it silently breaks all three. See Forum and tickets.
/api/records (7, all admin)
| Method | Path | Auth | Purpose | Notes |
|---|---|---|---|---|
| GET | / |
admin | List: slug, title, step count, publish state | |
| GET | /:slug |
admin | Full .rec.json, parsed |
|
| POST | / |
admin | Upsert | 201 created, 200 updated |
| PUT | /:slug |
admin | Rename only | A separate request so a 10 MB recording is not resent |
| POST | /:slug/publish |
admin | Write docs/recordings/<slug>.rec.json and return the embed snippet |
|
| POST | /:slug/unpublish |
admin | Remove the repo file again | |
| DELETE | /:slug |
admin | Delete the recording and its repo file |
lib/records.js sets MAX_BYTES = 50 * 1024 * 1024 (screenshots are data URLs, so the limit is generous); server.js allows a 64 MB JSON body. Publishing writes into docs/recordings/ and nothing else — one directory for every recording on the site, so a guide's embed is recordings/<slug>.rec.json whether the guide sits at the root of docs/ or three folders down. Guide embeds read recordings through the public path in routes/docs.js, never through this router. See Walkthrough recorder.
Redirects
| From | To | Status | Reason |
|---|---|---|---|
/admin, /records while gated |
Discord login, or the gallery | 302 | requireAdminPage |
/api/auth/discord/callback on success |
origin + returnTo |
302 | Sign-in flow; a non-admin headed to /admin lands on /profiles/hub?admin=denied instead |
/docs/<slug> non-canonical spelling |
canonical URL, query string preserved | 301 | One guide, one URL |
/docs/ |
/docs |
301 | Empty slug |
/oled.html |
/oled |
301 | Legacy extension URL |
/profile.html?id=<n> |
/profiles/<n> |
301 | Legacy query-string URL |
/profile.html (non-numeric id) |
/ |
301 | Nothing to show |
404 behaviour
There is no global catch-all. An unmatched path falls through to Express's default 404 — including unknown /api/* paths, which are HTML from Express rather than JSON. That is the one place a client has to check the status code instead of parsing a body.
Purpose-built 404s:
| Where | Response |
|---|---|
/docs/* miss (page, image, recording, og card) |
routes/docs.js render404() — full docs shell, noindex,nofollow, "No such guide", status 404 |
/profiles/:id, /users/:id, /forum/t/:id |
routes/seo.js notFoundHtml() — standalone 404 page with a link home, text/html |
/profiles/:id/share.png miss |
404 text/plain Not found |
/api/<router>/<id> miss |
{"error":"…"} |
/api/docs/<slug>/raw, /api/docs/<slug>/discord miss |
404, empty body |
Error responses
The only error handler in the app is the last middleware in server.js:169:
| Condition | Status | Body |
|---|---|---|
err.code === 'LIMIT_FILE_SIZE' (multer, 2 MB) |
413 | {"error":"File too large. Maximum size is 2 MB."} |
err.type === 'entity.too.large' (JSON body over 64 MB) |
413 | {"error":"Body too large. Maximum size is 64 MB."} |
| Anything else | 500 | {"error":"Server error."}, logged server-side |
Auth failures are plain JSON as listed under API routes. The upload 413 arrives from here rather than from the route, which is why the message is the app's wording and not multer's.
Shared chrome on every page
lib/header.js is the single source of the header, lib/footer.js the single source of the footer. Neither is hand-written into the pages.
- Header: brand
SWIFTPOINT//HUB, the six nav items, a theme toggle, aCtrl Kcommand palette, a Support link, the notification bell with panel and "Mark all read",#admin-btn(hidden until/api/auth/mesays admin), and the login/upload/user-chip cluster with the user menu (Your profile, Ticket queue, Write an update, Admin panel, Log out). - Footer: Browse profiles, Forum, Updates, Front page, Profile Builder, OLED Editor, Checker, Guides, Sitemap, Privacy, Imprint, plus the unofficial-community-project disclaimer.
scripts/sync-header.jswrites the generated header and footer into 12 pages between<!-- header:start -->/<!-- header:end -->and<!-- footer:start -->/<!-- footer:end -->, preserving each page's own.tbartool strip.npm run check:headerfails if any page has drifted, and it runs aspretest, so a page cannot ship with a stale header.public/js/theme.jsis blocking, setsdata-themeon<html>before first paint, cycles dark → light → system, and stores the choice undersph-theme.public/js/chrome.jsbuilds the command palette from the header itself, adds profile search (2+ characters, 180 ms debounce, top 6 results, with a sequence token so a slow response cannot overwrite a newer one) and derives the active nav item fromdata-paths.public/js/notifications.jspolls every 30 s, opens anEventSourceon/api/notifications/stream, reconnects after 3 s, caps the badge at99+, and toasts for 10 s with at most 4 on screen.
See also: API reference for bodies, Roles and permissions for the gates, SEO and analytics for meta and tracking, Security and privacy for CSP, the session cookie and returnTo sanitisation, Configuration and deployment for BASE_PATH and the cache policy, and Known gaps.