Tangly v0.2 ships richer code blocks, page chrome, and more — see what's new

Vite plugin

Virtual modules, HMR, MDX preprocessing, and static-asset middleware.

~ 2 min read

Vite plugin

packages/tangly/src/plugin/vite-plugin.ts is the bridge between user state (the manifest, theme, config) and the synthesized Astro runtime.

Module graph

flowchart LR
  subgraph user[User project]
    U1[docs.json]
    U2["*.mdx"]
    U3[images/, logo/, public/]
  end

  subgraph plugin[Vite plugin]
    P1[buildManifest]
    P2[chokidar watcher]
    P3[transform hook<br/>MDX preprocess]
    P4[middleware<br/>static assets]
    P5[component-shadow<br/>aliases]
  end

  subgraph virt[Virtual modules]
    V1[virtual:tangly/manifest]
    V2[virtual:tangly/routes]
    V3[virtual:tangly/config]
    V4[virtual:tangly/theme]
  end

  subgraph astro[Astro runtime]
    A1["[...slug].astro"]
    A2[Layout / TopNav / Sidebar]
  end

  U1 --> P1
  U2 --> P1
  P1 --> V1
  P1 --> V2
  P1 --> V3
  P1 --> V4
  P2 -. invalidate on change .-> P1
  U2 --> P3 --> A1
  U3 --> P4
  P5 -. user theme/ overrides .-> A2
  V1 --> A1
  V1 --> A2
  V4 --> A2

Virtual modules

Four virtual imports are exposed to the runtime:

IDExports
virtual:tangly/manifestThe full Manifest (config, pages, navigation, orphans, warnings, collections)
virtual:tangly/routes[{ slug, file }] for each page (drafts excluded in build mode)
virtual:tangly/configThe parsed DocsJson
virtual:tangly/theme{ themeName, userRoot }, used by Layout to pick the active theme’s CSS

Any .astro file in the runtime can import { manifest } from "virtual:tangly/manifest" and get a typed snapshot.

Hot reload

A chokidar watcher listens for changes in:

  • <userRoot>/docs.json
  • <userRoot>/**/*.mdx
  • <userRoot>/**/*.md
  • <userRoot>/tangly.config.ts
  • <userRoot>/_section.mdx and <userRoot>/**/_meta.json

On change, the manifest is invalidated, virtual modules are re-evaluated, and Vite is told to do a full reload via server.ws.send({ type: 'full-reload' }). Astro’s MDX HMR handles per-page MDX changes natively for sub-page edits.

Static-asset middleware

Mintlify projects use absolute paths for images: <img src="/images/foo.png">. Tangly’s middleware resolves these against the user root’s matching directory:

GET /images/foo.png  →  <userRoot>/images/foo.png
GET /logo/dark.svg   →  <userRoot>/logo/dark.svg
GET /favicon.ico     →  <userRoot>/<config.favicon>

Supported prefixes: images, logo, public, static, assets. Path traversal is blocked. Requests like /images/../../etc/passwd are rejected before any I/O.

Build-time variant

In production, packages/tangly/src/build-outputs/copy-assets.ts walks the same set of directories and copies them into dist/ so deployed pages see the same paths the dev server served.

MDX preprocess

The plugin’s transform() hook rewrites Mintlify-only quirks before MDX parses JSX:

  • <latex>...</latex>$$...$$ block math (so curly-brace LaTeX doesn’t trigger MDX expression parsing).
  • Relative Markdown image refs (![](../images/foo)) → absolute paths (/images/foo) so Astro’s asset pipeline doesn’t mis-resolve them in nested routes.

Both replacements run on raw MDX text but skip matches inside code spans and fenced code blocks (via replace-outside-code.ts), so docs describing these shims can quote the literal trigger patterns in backticks (this very paragraph does) without being silently rewritten.

More compatibility shims land here as we hit them.

Component shadowing

packages/tangly/src/plugin/component-shadow.ts rewrites Vite resolution for @tanglydocs/theme-ui/<Name>.astro:

  1. <userRoot>/theme/<Name>.{astro,tsx,jsx} (project-level override)
  2. @tanglydocs/theme-<active>/components/<Name>.astro (theme-level override)
  3. @tanglydocs/theme-ui/components/<Name>.astro (built-in default)

The lookup runs at module-resolution time, not import time, so overrides have zero runtime cost. Vite produces the same code as if you’d written the import directly.

Last updated Edit this page

Type to search…

↑↓ navigate open esc close