Instruction file imported from arcanewords-app/arcane-reader (
.cursor/rules/routing.mdc). Copyright stays with the author.
Routing (Canonical)
Sync policy
- This file is the source of truth for the route map.
- Implementations:
@src/client/AppRouter.tsx(frontend),@src/server.ts(API). docs/ROUTES.mdis a GitHub-friendly stub; do not edit it instead of this file.
When changing routes
- Update tables below (path, method/access, description).
- Update navigation flow if user journey changed.
- Change code in the same PR: client + server + this rule.
- Remove stale rows when endpoints are deleted.
Verification
- New route exists in code and in this file with identical path.
- Renames updated in client, server, and this file.
Frontend Routes (Client)
Defined in src/client/AppRouter.tsx.
| Path | Access | Component | Description |
|---|---|---|---|
/ |
guest+ | CatalogPage (HomePage) | Main catalog, tabs: All / My works |
/catalog |
guest+ | CatalogPage | Same as /. Query: filter=mine, author, translator, tag (entity IDs for filtering), sort=rating (highest rated; mutually exclusive with date sort in toolbar). In-app tab/filter sync preserves current path (/ or /catalog) to avoid remount |
/about |
guest+ | AboutPage | About Arcane |
/account-tiers |
guest+ | AccountTiersPage | Account level comparison (reader / author tiers) |
/contact |
guest+ | ContactPage | Contact info |
/privacy |
guest+ | PrivacyPage | Privacy policy |
/terms |
guest+ | TermsPage | Terms of service |
/news |
guest+ | NewsPage | News feed list |
/news/:slugOrId |
guest+ | NewsDetailPage | Single news post |
/profile |
user+ | ProfilePage | Profile: reading history, settings, avatar. Query: tab=reading (default, omitted) | quotes | settings | profile |
/translation-requests |
author+ | TranslationRequestsPage | Author board: open requests, interests, create project (AuthorGate) |
/projects |
author+ | ProjectsPage | Projects grid (AuthorGate) |
/admin/entities |
admin+ | AdminEntitiesRedirect | Redirect to /admin/entities/tag |
/admin/entities/:kind |
admin+ | AdminEntitiesPage | Global entities by kind: tag, author, translator (AdminGate) |
/admin/news |
admin+ | AdminNewsPage | Admin news feed and announcements (AdminGate) |
/admin/publications |
admin+ | AdminPublicationsPage | Catalog publication moderation (AdminGate) |
/admin/projects |
admin+ | AdminProjectsPage | All user projects: list, unpublish, delete (AdminGate) |
/admin/users |
admin+ | AdminUsersPage | User list and role management (AdminGate) |
/admin |
admin+ | AdminRedirect | Redirect to /admin/entities/tag |
/projects/:projectId |
author+ | ProjectPage | Project overview (AuthorGate). Query: search — pre-fill project-wide find modal (omit when empty) |
/projects/:projectId/chapters/:chapterId |
author+ | ChapterPage | Chapter editing (AuthorGate). Query: search — pre-fill find-in-chapter; paragraph — scroll to paragraph UUID (e.g. from project search) |
/projects/:projectId/chapters/:chapterId/reading |
author+ | ReadingModePage | Reading mode (AuthorGate). :chapterId in URL is canonical; prev/next/TOC update URL via route() (push) without page reload |
/projects/:projectId/reading |
author+ | ReadingModePage | Reading mode entry without chapter — replace-redirects to first available chapter URL |
/p/:publicationId |
guest+ | PublicationPage | Public publication page. Query (chapter list): q (search), translation=translated|all|untranslated (default translated), read=all|unread|read (default all; auth only for unread/read filter UI), order=asc|desc (default asc) |
/p/:publicationId/chapters/:chapterId/reading |
guest+ | PublicationReadingPage | Reading mode for public publication. :chapterId in URL is canonical; prev/next/TOC update URL via route() (push). Query: paragraph — numeric 0-based segment index (omit when 0); guest share links restore scroll via URL only |
Reading mode URL sync
See also @.cursor/rules/spa-navigation.mdc for the general SPA policy (push vs replace, path vs query, PR checklist).
- Chapter navigation (next/prev, TOC, keyboard):
ReadingModecallspreact-routerroute()to update:chapterIdin the path (history push). No full page reload. - Browser Back/Forward:
initialChapterIdfrom URL drivescurrentChapterIndex; cached chapter text stays in component state. - Share / F5: URL reflects the chapter being read.
- Author exit: returns to chapter editor for the current chapter (not the entry URL).
- Scroll position within chapter: optional
?paragraph=in URL for share/deep links only. Server paragraph bookmark deferred (see ADR reading-progress-watermark).
Access Levels
- guest+ - anyone (including unauthenticated)
- user+ - requires
role: useror higher (guest redirects to/?login=required) - author+ - requires
role: authoror higher (user sees UpgradeScreen on/translation-requestsand/projects/*) - admin+ - requires
role: admin(non-admin users see access denied)
Protected routes
When guest visits /profile, /translation-requests, /projects, /projects/*, /admin/entities, or /admin/*, redirect to /?login=required and open AuthModal.
API Routes (Server)
Defined in src/server.ts.
Validation (Zod)
Most API endpoints validate request body and query parameters using Zod schemas from src/api/schemas/. On validation failure, the API returns 400 with:
{
"error": "Validation failed",
"details": { "fieldName": ["error message"] }
}
Auth
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
- | Register new user |
| POST | /api/auth/login |
- | Login |
| POST | /api/auth/logout |
- | Logout |
| GET | /api/auth/me |
Bearer | Get current user (id, email, role, avatarUrl) |
| POST | /api/auth/refresh |
- | Refresh session (body: refresh_token) |
System
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/status |
- | System status (AI readiness, storage) |
| GET | /api/health |
- | Health check (shared Redis snapshot + recovery probe when Supabase down) |
Circuit breaker (503): requireHealthySupabase blocks mutating and private routes when Supabase is down (Redis cache outages do not trip the breaker). Shared health is stored in Redis (system:health, 60s TTL) so warm serverless instances agree. Exempt: /api/status, /api/health, and public read-only GET: /api/news, /api/news/*, /api/announcements/active, /api/publications, /api/publications/*, /api/public/entities, /api/public/entities/*.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /robots.txt |
- | Robots.txt |
| GET | /sitemap.xml |
- | Sitemap |
| GET | /api/robots |
- | Robots.txt (Vercel rewrite target) |
| GET | /api/sitemap |
- | Sitemap XML (Vercel rewrite target) |
Debug (dev only, NODE_ENV !== production)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /debug |
- | Debug console UI (redirect to dev app; static from dist/debug when built) |
| GET | /debug/clear |
- | Clear log buffer, redirect to /debug |
| GET | /debug/clear-prompts |
- | Clear LLM prompt captures, redirect to /debug?tab=prompts |
| GET | /debug/clear-http |
- | Clear HTTP captures, redirect to /debug?tab=http |
| GET | /api/debug/logs |
- | Log entries JSON (?newestFirst=1) |
| GET | /api/debug/status |
- | Buffer snapshot for agents (counts, last error, recent jobs) |
| GET | /api/debug/catalog |
- | Translation debug events + example queries |
| GET | /api/debug/agent/context |
- | Markdown agent context (?jobId= | ?traceId= | ?requestId=) |
| GET | /api/debug/query |
- | Filtered query (?kind=, ?format=agent, ?sort=asc) |
| GET | /api/debug/jobs/:jobId |
- | Async job aggregate: traces, logs, prompts, HTTP |
| GET | /api/debug/traces |
- | Trace index for waterfall |
| GET | /api/debug/traces/:id |
- | Unified trace detail: entries, summary, LLM + HTTP captures |
| GET | /api/debug/export |
- | Export markdown/json/cursor (?traceId=, ?format=) |
| GET | /api/debug/prompts |
- | Opt-in LLM captures (DEBUG_CAPTURE_LLM=1) |
| GET | /api/debug/http |
- | Opt-in HTTP captures (DEBUG_CAPTURE_HTTP=1) |
| POST | /api/debug/clear |
- | Clear log buffer (JSON) |
| POST | /api/debug/clear-http |
- | Clear HTTP captures (JSON) |
| POST | /api/debug/clear-prompts |
- | Clear LLM prompt captures (JSON) |
Vite dev proxy: /debug → debug app port 5174. See @docs/02-how-to/debug-translation.md and @.cursor/rules/debug.mdc.
Prompt Lab (dev only, NODE_ENV !== production)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /prompt-lab |
- | Prompt Lab UI (redirect to dev app; static from dist/prompt-lab when built) |
| GET | /api/prompt-lab/meta |
- | Supported pairs, stages, presets |
| GET | /api/prompt-lab/prompts/current |
- | Baseline system prompt from engine code |
| POST | /api/prompt-lab/prompts/preview-user |
- | Build user prompt preview |
| GET/POST/PUT/DELETE | /api/prompt-lab/prompts |
- | Saved prompt versions CRUD |
| GET/POST/PUT/DELETE | /api/prompt-lab/texts |
- | Saved test texts CRUD |
| GET/DELETE | /api/prompt-lab/runs |
- | Run history |
| GET | /api/prompt-lab/runs/:id/export |
- | Export run JSON |
| PATCH | /api/prompt-lab/runs/:id |
- | Update run displayName |
| GET | /api/prompt-lab/evaluations |
- | Evaluation history (?runId=) |
| GET | /api/prompt-lab/evaluations/:id |
- | Evaluation detail |
| DELETE | /api/prompt-lab/evaluations/:id |
- | Delete saved evaluation |
| POST | /api/prompt-lab/evaluate |
- | LLM translation review (saves to prompt_lab_evaluations) |
| POST | /api/prompt-lab/evaluate/preview |
- | Build evaluation system/user prompt (no LLM call) |
| POST | /api/prompt-lab/glossary/import |
- | Parse glossary JSON/CSV for snapshot |
| POST | /api/prompt-lab/run |
- | Ephemeral single-stage run (optional saveRun, runLabel) |
Vite dev proxy: /prompt-lab → port 5175. See @docs/02-how-to/prompt-lab.md and @.cursor/rules/prompt-lab.mdc.
User (requireAuth)
| Method | Path | Role | Description |
|---|---|---|---|
| GET | /api/user/token-usage |
user+ | Token usage (query: date) |
| GET | /api/user/token-usage/history |
user+ | Token usage history (query: days) |
| GET | /api/user/reading-history |
user+ | Reading history (publications) |
| GET | /api/user/quotes |
user+ | Saved quotes list |
| DELETE | /api/user/quotes/:quoteId |
user+ | Delete saved quote |
| GET | /api/user/translation-requests |
user+ | User's catalog translation requests |
| POST | /api/catalog/translation-requests |
user+ | Create catalog translation request (catalog suggest modal) |
| GET | /api/user/profile |
user+ | Profile (id, email, role, avatarUrl) |
| PUT | /api/user/profile |
user+ | Update profile (body: avatarUrl) |
| POST | /api/user/profile/avatar |
user+ | Upload avatar (multipart: avatar) |
| GET | /api/user/reader-settings |
user+ | User reader settings |
| PUT | /api/user/reader-settings |
user+ | Update user reader settings |
| GET | /api/user/translator-pseudonyms |
author+ | List own translator pseudonyms (query: includeHidden) |
| POST | /api/user/translator-pseudonyms |
author+ | Create translator pseudonym (multipart: name, description?, photo?) |
| PATCH | /api/user/translator-pseudonyms/:id |
author+ | Update own translator pseudonym |
| POST | /api/user/translator-pseudonyms/:id/hide |
author+ | Soft-hide own translator pseudonym |
| GET | /api/user/publications |
author+ | User's publications |
Translation request board (requireAuth + requireRole('author'))
| Method | Path | Description |
|---|---|---|
| GET | /api/translation-requests/board |
Open requests + interests aggregate (query: status, search, targetLanguage, mine, limit, offset) |
| POST | /api/translation-requests/:id/interests |
Express interest (translatorEntityId) |
| PATCH | /api/translation-requests/:id/interests/me |
Link projectId or update status |
| DELETE | /api/translation-requests/:id/interests/me |
Withdraw interest → withdrawn |
POST /api/projects body may include catalogTranslationRequestId and translatorEntityId to prefill from a request and link interest.
Projects (requireAuth + requireRole('author'))
| Method | Path | Description |
|---|---|---|
| GET | /api/projects |
List projects |
| POST | /api/projects |
Create project (409 PROJECT_LIMIT when cap reached) |
| POST | /api/projects/:id/clone |
Clone project snapshot (body: optional name; 409 PROJECT_LIMIT) |
| POST | /api/projects/:targetProjectId/transfer-from |
Copy chapters from another project (body: sourceProjectId, chapterIds[], includeGlossary?; 409 SAME_PROJECT, TARGET_LANGUAGE_MISMATCH; 500 TRANSFER_INCOMPLETE) |
| GET | /api/projects/:id |
Get project |
| PATCH | /api/projects/:id |
Rename project (body: name) |
| DELETE | /api/projects/:id |
Delete project |
| PUT | /api/projects/:id/settings |
Update project settings |
| PUT | /api/projects/:id/languages |
Update project translation language pair (source/target). Returns 409 (LANGUAGE_PAIR_LOCKED) if glossary non-empty or any chapter status ≠ pending. Clears agent cache + analysis Redis cache for project |
| GET | /api/projects/:id/settings/reader |
Get project reader settings |
| PUT | /api/projects/:id/settings/reader |
Update project reader settings |
| GET | /api/projects/:id/chapters/summary |
Chapters summary |
| GET | /api/projects/:id/search |
Search paragraphs in project (query: q, field=original|translated|both, caseSensitive, wholeWord, chapterFrom, chapterTo, chapterIds, offset, limit) |
| POST | /api/projects/:id/search/ai-replace |
AI smart replace for selected paragraphs (Author+; body: find, replaceHint?, preset, detail?, paragraphs[]) |
| POST | /api/projects/:id/chapters |
Upload chapters (multipart: file) |
| POST | /api/projects/:id/chapters/import |
Start async import job for .epub/.fb2/.csv (multipart: file) |
| GET | /api/projects/:id/import-jobs/:jobId |
Get import job status/progress (polling, compact query supported) |
| POST | /api/projects/:id/import-jobs/:jobId/cancel |
Request import job cancellation |
| GET | /api/projects/:projectId/jobs |
List all chapter jobs (analysis + translate) for project |
| GET | /api/projects/:projectId/chapters/:chapterId |
Get chapter |
| GET | /api/projects/:projectId/chapters/:chapterId/status |
Chapter status (when translating: includes chunksDone/totalChunks for progress) |
| DELETE | /api/projects/:projectId/chapters/:chapterId |
Delete chapter |
| POST | /api/projects/:projectId/chapters/duplicate |
Duplicate selected chapters at end of project (body: chapterIds[]) |
| POST | /api/projects/:projectId/chapters/bulk-delete |
Delete selected chapters (body: chapterIds[]) |
| PUT | /api/projects/:projectId/chapters/:chapterId/title |
Update chapter title |
| PUT | /api/projects/:projectId/chapters/:chapterId/number |
Update chapter number |
| PUT | /api/projects/:projectId/chapters/order |
Reorder chapters |
| POST | /api/projects/:projectId/chapters/analyze-batch |
Batch analyze chapters (body: chapterIds[], optional languagePair: { sourceLanguage, targetLanguage } — ephemeral override, does not update project). Sync by default; with Prefer: respond-async or ?async=1 returns 202 + jobId for polling. Async requires REDIS_URL; returns 503 if queue unavailable, 409 if user already has active analysis job |
| GET | /api/projects/:projectId/analysis-jobs/:jobId |
Get analysis job status/progress (polling, compact query supported) |
| POST | /api/projects/:projectId/analysis-jobs/:jobId/cancel |
Request analysis job cancellation |
| POST | /api/projects/:projectId/chapters/translate-batch |
Batch translate chapters (body: chapterIds[], translateOnlyEmpty?, translateChapterTitles? default true, stages?, optional languagePair). With ?async=1 or Prefer: respond-async returns 202 + jobId for polling. Async requires REDIS_URL; returns 503 if queue unavailable, 409 if user already has active translate job |
| GET | /api/projects/:projectId/translate-jobs/:jobId |
Get translate job status/progress (polling, compact query supported) |
| POST | /api/projects/:projectId/translate-jobs/:jobId/cancel |
Request translate job cancellation |
| POST | /api/projects/:projectId/chapters/:chapterId/translate |
Translate chapter (body: translateOnlyEmpty?, translateChapterTitles? default true, paragraphIds?, stages?, optional languagePair) |
| POST | /api/projects/:projectId/chapters/:chapterId/translate/cancel |
Cancel translation |
| POST | /api/projects/:projectId/chapters/:chapterId/translate/sync |
Manual sync translated chunks to paragraphs (recovery) |
| POST | /api/projects/:projectId/chapters/:chapterId/critic |
AI translation review (Author+); body: { force?: boolean }; returns { report, cached } |
| POST | /api/projects/:projectId/chapters/:chapterId/upload-translation |
Upload ready-made translation text |
| POST | /api/projects/:projectId/chapters/:chapterId/mark-as-translated |
Mark chapter as translated (copy original to translated) |
| POST | /api/projects/:projectId/chapters/mark-as-translated-batch |
Batch mark chapters as translated |
| GET | /api/projects/:projectId/chapters/:chapterId/stats |
Paragraph stats |
| PUT | /api/projects/:projectId/chapters/:chapterId/paragraphs/:paragraphId |
Update single paragraph |
| POST | /api/projects/:id/paragraphs/bulk-update |
Bulk update paragraph text (body: { updates: [{ chapterId, paragraphId, translatedText }] }) |
Glossary (requireAuth + requireRole('author'))
| Method | Path | Description |
|---|---|---|
| GET | /api/projects/:id/glossary |
List glossary |
| GET | /api/projects/:id/glossary/export |
Export glossary file (query: format=json|csv) |
| POST | /api/projects/:id/glossary/import |
Import glossary file (multipart: file — .json/.csv; append, skip duplicates) |
| POST | /api/projects/:id/glossary |
Add glossary entry |
| PUT | /api/projects/:projectId/glossary/:entryId |
Update glossary entry |
| DELETE | /api/projects/:projectId/glossary/:entryId |
Delete glossary entry |
| POST | /api/projects/:projectId/glossary/bulk-delete |
Bulk delete glossary entries (body: { entryIds: string[] }) |
| POST | /api/projects/:projectId/glossary/suggest-merges |
Suggest merges |
| POST | /api/projects/:projectId/glossary/merge |
Merge entries |
| POST | /api/projects/:projectId/glossary/:entryId/image |
Upload glossary image |
| DELETE | /api/projects/:projectId/glossary/:entryId/image/:imageIndex |
Delete one glossary image by index |
| DELETE | /api/projects/:projectId/glossary/:entryId/image |
Delete glossary image |
Admin (requireAuth + requireRole('admin'))
| Method | Path | Description |
|---|---|---|
| POST | /api/admin/entities |
Create global public entity (tag, author, translator) |
| PATCH | /api/admin/entities/:id |
Update global public entity (name, description, photo) |
| DELETE | /api/admin/entities/:id |
Delete global public entity (409 if used by publications) |
| GET | /api/admin/entities/:id/usage |
Get entity usage count (publications referencing it) |
| GET/POST | /api/admin/news |
List all news posts (query: status, search, limit, offset) / create draft |
| GET/PATCH/DELETE | /api/admin/news/:id |
News CRUD |
| POST | /api/admin/news/:id/publish |
Publish draft |
| POST | /api/admin/news/:id/translate |
Stub 501 (future AI translation) |
| GET/POST | /api/admin/announcements |
List / create announcement alert |
| PATCH/DELETE | /api/admin/announcements/:id |
Update / delete alert |
| POST | /api/admin/announcements/from-news/:newsId |
Create alert prefilled from news |
| GET | /api/admin/publications |
List all publications (query: status, search, targetLanguage, limit, offset) |
| POST | /api/admin/publications/:id/unpublish |
Force unpublish publication |
| GET | /api/admin/projects |
List all projects (query: search, publicationStatus, targetLanguage, limit, offset) |
| POST | /api/admin/projects/:id/unpublish |
Force unpublish project's publication |
| DELETE | /api/admin/projects/:id |
Force delete project (cascade chapters, glossary, publication) |
| GET | /api/admin/translation-requests |
List translation requests (query: status, search, targetLanguage, limit, offset) |
| PATCH | /api/admin/translation-requests/:id |
Update request (status, adminNotes, linkedPublicationId) |
| DELETE | /api/admin/translation-requests/:id |
Delete request (only rejected or fulfilled; 409 otherwise) |
| GET | /api/admin/users |
List users with profiles (query: search, limit, offset) |
| PATCH | /api/admin/users/:id/role |
Update user role (body: { role }) |
News & announcements (public)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/news |
- | Published news list. Query: limit, offset, category |
| GET | /api/news/:idOrSlug |
- | Single published news post |
| GET | /api/announcements/active |
optionalAuth | Active banner for user role (null if none) |
| POST | /api/announcements/:id/dismiss |
user+ | Record dismiss { contentVersion } |
Cache invalidation notes
- Mutation endpoints in
projects,chapters,glossary,cover/metadata,publish, andadmin/newsflows invalidate Redis-backed caches for:- public metadata entities (
/api/public/entities) after admin create, - news list/post and active announcements after admin news/alert mutations,
- user project views (
/api/projects,/api/projects/:id, summaries), - publication pages (
/api/publications/*) when relevant, - user reading/token aggregates where relevant.
- public metadata entities (
- This prevents stale UI after successful write operations.
Cover & Metadata (requireAuth + requireRole('author'))
| Method | Path | Description |
|---|---|---|
| POST | /api/projects/:projectId/cover |
Upload cover (multipart) |
| DELETE | /api/projects/:projectId/cover |
Delete cover |
| PUT | /api/projects/:projectId/metadata |
Update metadata |
Export
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/projects/:id/export |
author | Export project (EPUB/FB2) - deprecated, use build-exports for publications |
| GET | /api/projects/:id/export/download |
author | Download project export (query: path) |
| POST | /api/publications/:id/build-exports |
author | Build EPUB/FB2 once, save to publication (body: { formats?: ['epub','fb2'] }) |
| PATCH | /api/publications/:id |
author | Update display settings (body: { showGlossary?: boolean }). Owner only. |
| GET | /api/publications/:id/download |
user | Download built publication export (query: format=epub|fb2) |
Publications (public)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/publications |
- | List published publications. Query: limit, offset, orderBy (published_at | created_at | rating), orderAsc, author, translator, tag (entity IDs). Response includes translatedChapterCount, ratingAvg, ratingCount (display when count ≥ 5). |
| GET | /api/publications/:id |
- | Get publication (includes ratingAvg, ratingCount, ratingBayesian) |
| GET | /api/publications/:id/chapters |
- | Publication chapters |
| GET | /api/publications/:id/chapters/:chapterId |
- | Chapter content |
| GET | /api/publications/:id/glossary |
- | Publication glossary (returns [] when showGlossary=false) |
| GET | /api/publications/:id/read-progress |
optional | Read progress watermark (lastReadChapterNumber) |
| PATCH | /api/publications/:id/read-progress |
user+ | Update watermark (chapterNumber, mode: complete | set) |
| DELETE | /api/publications/:id/read-progress |
user+ | Reset progress |
| GET | /api/publications/:id/rating |
optional | User rating status (userScore, eligibility) |
| PUT | /api/publications/:id/rating |
user+ | Upsert 1–5 rating (body: { score }; requires lastReadChapterNumber >= 1; not owner) |
| DELETE | /api/publications/:id/rating |
user+ | Remove own rating |
| POST | /api/publications/:id/quotes |
user+ | Save quote from reading selection (body: chapterId, chapterNumber, quoteText, anchor offsets) |
| POST | /api/publications/:id/chapters/:chapterId/read |
user+ | Deprecated — completes chapter via watermark |
| PATCH | /api/publications/:id/reading-position |
user+ | Deprecated — returns 410 |
Public metadata entities (public)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/public/entities |
- | List global public entities. Query: kind, search, limit, offset, or ids (comma-separated UUIDs, max 50; when set, returns those active entities and ignores kind/search/limit/offset) |
| GET | /api/public/entities/:id |
- | Get single public entity by id |
Publish (requireAuth + requireRole('author'))
| Method | Path | Description |
|---|---|---|
| POST | /api/projects/:projectId/publish |
Create/update publication (body: optional title, description, coverImageUrl, sourceUrl, entity IDs, translationStatus, …). Response includes sourceUrl when set |
| DELETE | /api/projects/:projectId/publish |
Unpublish |
| GET | /api/projects/:projectId/publication |
Get project's publication |
Translation Reports (requireAuth + requireRole('author'))
| Method | Path | Description |
|---|---|---|
| GET | /api/projects/:id/reports-count |
Get pending reports count for project |
| GET | /api/projects/:id/reports |
List translation reports for project |
| PATCH | /api/projects/:id/reports/:reportId |
Update report status (body: { status: 'pending' | 'reviewed' | 'resolved' }) |
| DELETE | /api/projects/:id/reports/:reportId |
Delete report |
SPA fallback
| Method | Path | Description |
|---|---|---|
| GET | /p/:publicationId |
SPA fallback for publication |
| GET | /p/:publicationId/chapters/:chapterId/reading |
SPA fallback for reading |
| GET | * |
Fallback: index.html |
SEO (SSR, sitemap, canonical)
Implemented in src/server.ts. Audits: .cursor/skills/seo/SKILL.md, .cursor/rules/seo.mdc.
Crawl files
| Path | Notes |
|---|---|
/robots.txt, /api/robots |
Allow /; disallow /profile, /translation-requests, /projects, /admin; Sitemap: absolute URL |
/sitemap.xml, /api/sitemap |
Also api/sitemap.ts on Vercel |
/google16d698644e51469e.html |
Google Search Console ownership verification (public/ → dist/client) |
/yandex_3d5cc7aa18d6250e.html |
Yandex Webmaster ownership verification (public/ → dist/client) |
/google16d698644e51469e.html |
Google Search Console ownership verification (public/ → dist/client) |
/yandex_3d5cc7aa18d6250e.html |
Yandex Webmaster ownership verification (public/ → dist/client) |
Sitemap contents and limits
/(priority 1.0) plus static:/about,/contact,/privacy,/terms,/catalog,/news,/account-tiers- Each published publication:
/p/{slugOrId}(up to 1000 fromlistPublicationsPublic) - First translated chapter reading URL for at most 100 publications (
SITEMAP_CHAPTER_PUBS_LIMIT) - Published news posts:
/news/{slugOrId}(up to 100,SITEMAP_NEWS_LIMIT)
SSR HTML (meta for crawlers)
| Path | Behavior |
|---|---|
/, /catalog, /about, /contact, /privacy, /terms, /news, /account-tiers |
serveStaticPageHtml — title, description, og, canonical |
/catalog |
Canonical URL = site root / (avoid duplicate home vs catalog) |
/news/:slugOrId |
serveNewsDetailHtml — meta, hidden crawler content, NewsArticle JSON-LD |
/p/:publicationId |
servePublicationHtml — meta, hidden <main class="publication-page-seo">, Book + BreadcrumbList JSON-LD |
/p/:publicationId/chapters/:chapterId/reading |
Chapter title/description; chapter breadcrumb |
Client navigation updates head via usePageMeta.ts (publications, news detail) and useStaticPageMeta.ts (static info pages); direct loads and bots rely on SSR above.
Vercel rewrites
In vercel.json, /, /catalog, /about, /contact, /privacy, /terms, /news, /news/:slug*, /account-tiers, and /p/* route to api/index (Express SSR). Other SPA paths fall through to static index.html.
Shared SEO modules
- src/shared/robotsTxt.ts —
buildRobotsTxtfor Express and api/robots.ts - src/shared/staticPageMeta.ts — static page titles/descriptions for SSR and client
Auth areas
/projects/*, /profile, /translation-requests, /admin/* are disallowed in robots and not in sitemap (author workspace, not public SEO targets).
Navigation Map (Key Flows)
User journey tables (header, catalog, projects, publication): @docs/_canonical/rules/routing-nav.md.
Token Usage
Token usage indicator is shown only on paths where it's relevant. See src/client/utils/tokenUsagePaths.ts:
/projects- projects grid/projects/:id- project page (except/reading)
Monorepo apps (not arcane-reader)
Web scraper
Moved to standalone repo arcane-scraper (@arcane/scraper + @arcane/scraper-console). Not registered in arcane-reader src/server.ts.
Async Jobs (BullMQ Worker)
Async analysis and translate jobs require a separate Worker process and Redis (REDIS_URL).
- Local dev:
npm run dev:full— API, client, and worker against Docker Postgres; or runnpm run workerin a separate terminal. Live prod DB:npm run dev:full:prod(.env.prod.local). - Production:
npm run start:worker- run alongsidenpm run start(API). - Queues:
chapter-analysis,chapter-translate. One active job per user per type (analysis/translate).
Related Files
src/client/AppRouter.tsx- frontend routessrc/client/components/Auth/AdminGate.tsx- admin role gatesrc/server.ts- API routessrc/api/routes/index.ts- registers route modules (chapters.ts,chapterImport.ts,chapterReports.ts, …)src/worker.ts- BullMQ worker entry pointsrc/services/chapterQueue.ts- BullMQ queues (analysis, translate)src/middleware/auth.ts- requireAuth, optionalAuth, requireRolesrc/client/components/Auth/AuthorGate.tsx- author+ role gatesrc/client/pages/AdminEntitiesPage.tsx- admin entities pagesrc/client/utils/tokenUsagePaths.ts- token usage display paths