Developer Docs

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:

  1. Build bloat โ€” Every documentation change triggered a full SvelteKit rebuild
  2. Coupling โ€” Documentation routes, utilities, and styles cluttered the app codebase
  3. Deployment โ€” Docs couldn't be deployed independently; a typo fix required a full frontend deploy
  4. 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-mode class toggled via ThemeToggle, persisted in localStorage under mall-theme, supports prefers-color-scheme auto-detection
  • Flash prevention โ€” BaseLayout includes 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:

  1. Build stage โ€” node:20-alpine with pnpm, runs pnpm build
  2. Serve stage โ€” nginx:alpine, copies dist/ 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:

  1. Frontmatter extraction โ€” YAML block parsed into metadata object
  2. Cross-portal URL rewriting โ€” remarkPortalUrls remark plugin rewrites hardcoded production URLs (https://developer.mallnline.com, https://support.mallnline.com, https://mallnline.com) to env-driven values at build time via unist-util-visit AST traversal
  3. GitHub-style alerts โ€” > [!NOTE], > [!WARNING], etc. converted to styled <div class="doc-alert-*"> blocks
  4. Table of Contents โ€” H2/H3 headings extracted with slug-ified IDs for anchor links
  5. Mermaid diagrams โ€” language-mermaid code blocks lazy-loaded client-side via dynamic import('mermaid'); theme auto-switches between default and dark based on body.dark-mode
  6. Heading anchors โ€” All H2/H3 elements get id attributes for deep linking and scroll-margin-top for 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.