Help system architecture
How the public Starlight site and the in-portal help drawer share Markdown content via the dual-folder model.
End-user documentation lives in manual/src/content/. The same repository feeds two consumption surfaces via a deliberate two-folder split:
| Folder | Surface | Audience | Format |
|---|---|---|---|
manual/src/content/docs/admin/, customer/, dev/ | Public Starlight site (help.jattlogistics.com) | Prospects, customers, devs, the open web | Rich Markdown / MDX — screenshots, components, callouts allowed |
manual/src/content/drawer/admin/, customer/ | In-portal help drawer (topbar ?) | Logged-in ops + customer users mid-task | Plain Markdown — compact, no MDX, no screenshots |
The drawer/ and docs/{admin,customer}/ folders use identical slugs so a drawer page at drawer/admin/jobs/create-booking.md has its public counterpart at docs/admin/jobs/create-booking.md (mapped to the URL /admin/jobs/create-booking/ on the public site).
The docs/dev/ section has no drawer/ counterpart — devs use the public site directly.
Surface 1: public Starlight site
Section titled “Surface 1: public Starlight site”Built from manual/src/content/docs/ via Astro + Starlight. Three top-level sections in the sidebar:
- Admin Guide (
docs/admin/) - Customer Guide (
docs/customer/) - Dev Manual (
docs/dev/)
Each guide is autogenerated from its folder structure (see manual/astro.config.mjs). Frontmatter sidebar.order controls page ordering.
Deployed by a dedicated Railway service in the same Railway project as the Flask app:
- Root Directory:
manual - Watch Paths:
manual/** - Builder:
manual/Dockerfile(multi-stage: Node build →serve dist) - Custom Domain:
help.jattlogistics.com(prod),help.portals.jattlogistics.com(staging)
Surface 2: in-portal help drawer
Section titled “Surface 2: in-portal help drawer”When a logged-in user clicks the ? icon in the topbar, static/js/help-drawer.js:
- Maps the current URL → docs slug via a longest-prefix lookup (
HELP_MAPin the JS). - Fetches
/api/help/<slug>from the Flask backend. - Injects the returned HTML into the drawer body.
- Sets the footer CTA to
<HELP_DOCS_BASE>/<slug>/so users can “Read full guide” on the public site.
api/help_api.py:
DOCS_ROOT = manual/src/content/drawer/(the drawer folder, not docs/)ALLOWED_SECTIONS = ('admin', 'customer')—dev/is never readable via the drawer- Path-traversal safe: parts filter + post-resolve
relative_to(DOCS_ROOT)check - Reads with
python-frontmatter, renders body viamarkdown(fenced_code,tables,attr_list,toc) - Decorated with
@login_required+@cached_route(timeout=3600)
The two-folder rule
Section titled “The two-folder rule”Drawer is the floor, public site is the ceiling.
- Whatever appears in
drawer/<slug>.mdMUST also be true on the public Starlight page. - The public version can ADD: screenshots, MDX components, callouts, alternative flows, expanded walkthroughs.
- The public version must never CONTRADICT the drawer.
This means:
- Authors write the drawer version first (canonical content).
- Once stable, enhance the docs version with rich features for the public site.
- If only one version exists, it MUST be the drawer one.
Cache freshness
Section titled “Cache freshness”The in-portal drawer caches each rendered HTML response for 1 hour per Flask worker (@cached_route from utils/cache_config.py). Edits to a .md file appear:
- Immediately on the public site — Railway redeploys the manual service on push.
- Up to 1 hour later in the drawer — until the cache entry expires or the Flask worker recycles.
If the lag becomes painful, drop the TTL in api/help_api.py (the @cached_route(timeout=...) decorator argument) or add a webhook from the manual service → Flask service that invalidates the cache on deploy.
File map
Section titled “File map”manual/├── astro.config.mjs ← Starlight sidebar (3 sections)├── package.json├── Dockerfile ← Used by the second Railway service├── railway.json├── README.md ← Authoring guide└── src/ ├── content.config.ts ← Starlight content collection config ├── content/ │ ├── docs/ ← Starlight reads from here │ │ ├── index.md │ │ ├── admin/... │ │ ├── customer/... │ │ └── dev/... ← Dev Manual (public-only) │ └── drawer/ ← Flask reads from here │ ├── admin/... │ └── customer/... └── styles/custom.css
api/help_api.py ← /api/help/<path:slug>static/js/help-drawer.js ← URL→slug map, fetch + injecttemplates/dashboard_base.html ← Drawer DOM + topbar ? wiringutils/config.py ← help_docs_base propertyutils/context_processors.py ← app_config Jinja2 globalEnvironment variables
Section titled “Environment variables”| Env | Default | Where used |
|---|---|---|
HELP_DOCS_BASE | https://help.jattlogistics.com | Public URL base; drawer CTA → <HELP_DOCS_BASE>/<slug>/ |
Set per environment in Railway (staging → help.portals.jattlogistics.com, prod → help.jattlogistics.com).
PR convention
Section titled “PR convention”If your change affects user-visible behaviour, update the matching page in manual/src/content/drawer/<section>/... in the same PR. Optionally enhance the matching docs/<section>/... page with screenshots / MDX.
If your change affects dev workflow / architecture / API, update or add a page in manual/src/content/docs/dev/<subsection>/.... No drawer/ counterpart needed for dev content.