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:

  • `
......

......` 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.

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
↑↓ navigate open esc close