Documentation Portal Architecture
The Mallnline platform documentation is split into two independent Astro 6 static sites, decoupled from the main SvelteKit frontend monolith (ngwenya-front). This separation enables faster builds, independent deployment, and cleaner ownership boundaries as the team onboards new developers ahead of native app development.
| Portal | Repo | Production URL | Local Dev | Port |
|---|---|---|---|---|
| Developer Portal | ngwenya-dev |
developer.mallnline.com |
localhost:4321 |
4321 |
| Support Center | ngwenya-support |
support.mallnline.com |
localhost:4322 |
4322 |
| Main Frontend | ngwenya-front |
mallnline.com |
localhost:5173 |
5173 |
Why the Split?
Before this migration, all ~150 markdown docs lived inside ngwenya-front under src/lib/docs/ and src/lib/support/manuals/, rendered by custom Svelte components (DocRenderer, DocTOC). This created several problems:
- Build bloat โ Every documentation change triggered a full SvelteKit rebuild
- Coupling โ Documentation routes, utilities, and styles cluttered the app codebase
- Deployment โ Docs couldn't be deployed independently; a typo fix required a full frontend deploy
- Onboarding overhead โ New developers had to navigate a monolith to find docs
The Astro migration solves all of these with sub-2-second builds, zero-config content collections, and independent nginx-served static deployments.
Repository Structure
Developer Portal (`ngwenya-dev`)
ngwenya-dev/
โโโ src/
โ โโโ content/docs/ # 86+ markdown articles
โ โ โโโ overview.md # Landing page content
โ โ โโโ *.md # Architecture, Management, etc.
โ โ โโโ subgraphs/ # Per-subgraph references
โ โ โโโ auth.md
โ โ โโโ uchat.md
โ โ โโโ ...
โ โโโ components/
โ โ โโโ Sidebar.astro # Auto-discovered nav from frontmatter
โ โ โโโ DocTOC.astro # IntersectionObserver-based TOC
โ โ โโโ ThemeToggle.astro # Dark mode toggle (localStorage)
โ โโโ layouts/
โ โ โโโ BaseLayout.astro # HTML shell, fonts, theme init
โ โ โโโ DocLayout.astro # Sidebar + content + mobile topbar
โ โโโ pages/
โ โ โโโ index.astro # Renders overview.md
โ โ โโโ [slug].astro # Dynamic doc pages (SSG)
โ โโโ styles/
โ โ โโโ tokens.css # $mall design tokens (light + dark)
โ โ โโโ global.css # Resets, typography, scrollbar
โ โ โโโ doc-renderer.css # Headings, code, tables, alerts
โ โโโ utils/
โ โโโ docUtils.ts # Markdown parser, frontmatter, TOC
โ โโโ portalUrls.ts # Env-driven external URLs
โโโ .env # Local dev URL overrides
โโโ Dockerfile # pnpm build โ nginx serve
โโโ astro.config.mjs # SSG output, Svelte integration
Support Center (`ngwenya-support`)
Same structure, with src/content/manuals/ instead of src/content/docs/, and a SupportLayout with topic-card home page instead of the doc index.
Environment-Driven URL Management
All cross-portal links use centralized configuration rather than hardcoded production URLs. This enables seamless local development where all three apps run simultaneously.
IMPORTANT
Markdown content uses production URLs (e.g. https://support.mallnline.com/slug) which are automatically rewritten at build time by the remarkPortalUrls remark plugin. Authors should NOT try to use import.meta.env or template literals in .md files โ the remark plugin handles URL portability transparently.
URL Config Files
Each repo has a src/utils/portalUrls.ts (or $lib/config/portalUrls.ts in SvelteKit):
ngwenya-front โ src/lib/config/portalUrls.ts
import { PUBLIC_DEVELOPER_PORTAL_URL, PUBLIC_SUPPORT_CENTER_URL } from '$env/static/public';
export const DEVELOPER_PORTAL_URL =
PUBLIC_DEVELOPER_PORTAL_URL || 'https://developer.mallnline.com';
export const SUPPORT_CENTER_URL =
PUBLIC_SUPPORT_CENTER_URL || 'https://support.mallnline.com';
ngwenya-dev โ src/utils/portalUrls.ts
export const MAIN_APP_URL =
import.meta.env.PUBLIC_MAIN_APP_URL || 'https://mallnline.com';
export const SUPPORT_CENTER_URL =
import.meta.env.PUBLIC_SUPPORT_CENTER_URL || 'https://support.mallnline.com';
ngwenya-support โ src/utils/portalUrls.ts
export const MAIN_APP_URL =
import.meta.env.PUBLIC_MAIN_APP_URL || 'https://mallnline.com';
export const DEVELOPER_PORTAL_URL =
import.meta.env.PUBLIC_DEVELOPER_PORTAL_URL || 'https://developer.mallnline.com';
Environment Variable Matrix
| Variable | ngwenya-front | ngwenya-dev | ngwenya-support | Production Default |
|---|---|---|---|---|
PUBLIC_MAIN_APP_URL |
โ | โ | โ | https://mallnline.com |
PUBLIC_DEVELOPER_PORTAL_URL |
โ | โ | โ | https://developer.mallnline.com |
PUBLIC_SUPPORT_CENTER_URL |
โ | โ | โ | https://support.mallnline.com |
Local `.env` Overrides
Each repo's .env points to the corresponding localhost ports:
# ngwenya-front/.env
PUBLIC_DEVELOPER_PORTAL_URL=http://localhost:4321
PUBLIC_SUPPORT_CENTER_URL=http://localhost:4322
# ngwenya-dev/.env
PUBLIC_MAIN_APP_URL=http://localhost:5173
PUBLIC_SUPPORT_CENTER_URL=http://localhost:4322
# ngwenya-support/.env
PUBLIC_MAIN_APP_URL=http://localhost:5173
PUBLIC_DEVELOPER_PORTAL_URL=http://localhost:4321
IMPORTANT
When deploying to production, do not set these env vars โ the code defaults to the correct subdomain URLs. Only set them for local dev or staging environments.
Local Development
Starting All Five Apps
The ngwenya-front/Makefile orchestrates all five frontend applications:
# Start everything: frontend (5173) + Tower (5170) + Deck (5171) + dev portal (4321) + support center (4322)
make ui
# Or start apps individually
make tower-ui # localhost:5170
make deck-ui # localhost:5171
make dev-portal # localhost:4321
make support-center # localhost:4322
# Start both portals without the main frontend
make docs-dev
# Build both for production
make docs-build
# Install workspace dependencies (resolves @ngwenya/queries)
make install
The make ui target starts the doc portals, Tower, and Deck as background processes, then runs the main Vite dev server in the foreground. Ctrl+C kills all five. The kill-ports target cleans up all ports (5170โ5179, 4173, 4321, 4322).
NOTE
make install now runs pnpm install from the workspace root to correctly resolve @ngwenya/queries via the workspace protocol. All five apps share this dependency.
Adding New Documentation
Both portals use zero-config content discovery. Drop a .md file into the content directory with valid frontmatter and it's automatically picked up by the sidebar, routing, and build:
# Developer doc
vi ngwenya-dev/src/content/docs/my-new-feature.md
# Support manual
vi ngwenya-support/src/content/manuals/my-new-guide.md
See the agent workflows at .agent/workflows/create-developer-doc.md and .agent/workflows/create-support-doc.md for the full content creation process.
Design System Parity
Both portals share the same $mall design token system as the main frontend:
- tokens.css โ Color palette (light + dark), typography, spacing, radius, shadows
- Dark mode โ
body.dark-modeclass toggled viaThemeToggle, persisted inlocalStorageundermall-theme, supportsprefers-color-schemeauto-detection - Flash prevention โ
BaseLayoutincludes an inline<script is:inline>that runs before paint to set the correct class
The adaptive token system provides automatic theming:
:root {
--mall-bg: var(--mall-light-bg);
--mall-text: var(--mall-light-text);
}
body.dark-mode {
--mall-bg: var(--mall-dark-bg);
--mall-text: var(--mall-dark-text);
}
Build & Deployment
Static Site Generation (SSG)
Both portals output fully static HTML โ no server-side rendering needed:
cd ngwenya-dev && pnpm build # โ dist/ (106 pages, ~6s)
cd ngwenya-support && pnpm build # โ dist/ (55 pages, ~1s)
Docker/Podman
Each repo includes a two-stage Dockerfile:
- Build stage โ
node:20-alpinewith pnpm, runspnpm build - Serve stage โ
nginx:alpine, copiesdist/into/usr/share/nginx/html
podman build -t mallnline-dev-portal ./ngwenya-dev
podman build -t mallnline-support-center ./ngwenya-support
Production Routing
On the VPS, configure the reverse proxy (Nginx/Caddy) to route:
| Subdomain | Container | Port |
|---|---|---|
developer.mallnline.com |
mallnline-dev-portal |
80 |
support.mallnline.com |
mallnline-support-center |
80 |
Content Rendering Pipeline
Both portals share a docUtils.ts that handles the markdown-to-HTML pipeline:
- Frontmatter extraction โ YAML block parsed into metadata object
- Cross-portal URL rewriting โ
remarkPortalUrlsremark plugin rewrites hardcoded production URLs (https://developer.mallnline.com,https://support.mallnline.com,https://mallnline.com) to env-driven values at build time viaunist-util-visitAST traversal - GitHub-style alerts โ
> [!NOTE],> [!WARNING], etc. converted to styled<div class="doc-alert-*">blocks - Table of Contents โ H2/H3 headings extracted with slug-ified IDs for anchor links
- Mermaid diagrams โ
language-mermaidcode blocks lazy-loaded client-side via dynamicimport('mermaid'); theme auto-switches betweendefaultanddarkbased onbody.dark-mode - Heading anchors โ All H2/H3 elements get
idattributes for deep linking andscroll-margin-topfor sticky header offset
remarkPortalUrls Plugin
Located at src/utils/remarkPortalUrls.mjs in both portals. Registered in astro.config.mjs under markdown.remarkPlugins.
// Rewrites markdown link URLs at build time:
// https://developer.mallnline.com/slug โ $PUBLIC_DEVELOPER_PORTAL_URL/slug
// https://support.mallnline.com/slug โ $PUBLIC_SUPPORT_CENTER_URL/slug
// https://mallnline.com/slug โ $PUBLIC_MAIN_APP_URL/slug
This means markdown authors can write natural, readable links using production URLs, and the build pipeline ensures they resolve correctly in any environment.
Related
- Local Development Environment โ Full-stack Podman setup including Make targets
- Informational Pages โ The frontend's public-facing About, Help, For Business pages that link to these portals
- Beta Readiness Checklist โ Launch checklist including documentation completeness
- Corporate Identity โ Design system and terminology standards used across all portals