Skip to content

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:

FolderSurfaceAudienceFormat
manual/src/content/docs/admin/, customer/, dev/Public Starlight site (help.jattlogistics.com)Prospects, customers, devs, the open webRich Markdown / MDX — screenshots, components, callouts allowed
manual/src/content/drawer/admin/, customer/In-portal help drawer (topbar ?)Logged-in ops + customer users mid-taskPlain 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.

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)

When a logged-in user clicks the ? icon in the topbar, static/js/help-drawer.js:

  1. Maps the current URL → docs slug via a longest-prefix lookup (HELP_MAP in the JS).
  2. Fetches /api/help/<slug> from the Flask backend.
  3. Injects the returned HTML into the drawer body.
  4. 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 via markdown (fenced_code, tables, attr_list, toc)
  • Decorated with @login_required + @cached_route(timeout=3600)

Drawer is the floor, public site is the ceiling.

  • Whatever appears in drawer/<slug>.md MUST 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.

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.

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 + inject
templates/dashboard_base.html ← Drawer DOM + topbar ? wiring
utils/config.py ← help_docs_base property
utils/context_processors.py ← app_config Jinja2 global
EnvDefaultWhere used
HELP_DOCS_BASEhttps://help.jattlogistics.comPublic URL base; drawer CTA → <HELP_DOCS_BASE>/<slug>/

Set per environment in Railway (staging → help.portals.jattlogistics.com, prod → help.jattlogistics.com).

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.