Monorepo & Multi-Project Setups
mdt supports monorepos where each package manages its own templates independently. The key mechanism is sub-project boundaries: any directory containing its own mdt.toml is treated as a separate mdt project.
How sub-project boundaries work
When mdt scans a directory tree, it stops descending into any subdirectory that contains an mdt.toml file. That subdirectory becomes its own isolated scope with its own sources, targets, data files, and configuration.
my-monorepo/
mdt.toml # root project
.templates/
template.t.md # root sources
readme.md # root targets
packages/
lib-a/
mdt.toml # lib-a is a separate project
.templates/
template.t.md # lib-a sources
readme.md # lib-a targets
lib-b/
mdt.toml # lib-b is a separate project
.templates/
template.t.md # lib-b sources
readme.md # lib-b targets
lib-c/
readme.md # NO mdt.toml — belongs to root project
Running mdt update from the monorepo root updates targets in readme.md and packages/lib-c/readme.md, but not in packages/lib-a/ or packages/lib-b/. Those are separate projects.
To update lib-a, run mdt update from inside packages/lib-a/, or use the --path flag:
mdt update --path packages/lib-a
Setting up a monorepo
Step 1: Create an mdt.toml in each package
Each package that needs its own template scope gets an mdt.toml. Even an empty file is enough to establish a boundary:
# packages/lib-a/mdt.toml
Add configuration as needed:
# packages/lib-a/mdt.toml
[data]
cargo = "Cargo.toml"
Step 2: Create template files per package
Each sub-project has its own *.t.md files with its own source blocks:
<!-- packages/lib-a/.templates/template.t.md -->
<!-- {@install} -->
cargo add lib-a
<!-- {/install} -->
<!-- packages/lib-b/.templates/template.t.md -->
<!-- {@install} -->
cargo add lib-b
<!-- {/install} -->
Source names only need to be unique within a project scope. Both lib-a and lib-b can have an {@install} provider without conflict.
Step 3: Run updates per package or use a script
Update each package individually:
mdt update --path packages/lib-a
mdt update --path packages/lib-b
Or use a script to update all packages:
#!/bin/sh
for dir in packages/*/; do
if [ -f "$dir/mdt.toml" ]; then
mdt update --path "$dir"
fi
done
Shared templates across packages
Sub-project boundaries are strict. A source in the root .templates/template.t.md is not visible to consumers inside packages/lib-a/. Each scope is fully isolated.
If you need shared content across packages, you have a few options:
Option 1: Use block arguments for parameterized content
Define a parameterized source at the root level and use it for files that belong to the root scope:
<!-- {@badge:"crate_name"} -->
[](https://crates.io/crates/{{ crate_name }})
<!-- {/badge} -->
For sub-projects, duplicate the source in each sub-project’s template file. This is intentional — each project is self-contained.
Option 2: Duplicate sources where needed
Copy the source block into each sub-project’s template file. While this creates duplication in template files, the target blocks throughout each project stay in sync with their local provider — which is mdt’s primary guarantee.
Option 3: Keep shared content at the root scope
If files consuming shared content don’t live inside a sub-project directory, they can all reference the root-level sources. Structure your project so that shared docs live outside sub-project boundaries.
CI checks in a monorepo
Run mdt check for each sub-project in CI:
- name: check root docs
run: mdt check
- name: check lib-a docs
run: mdt check --path packages/lib-a
- name: check lib-b docs
run: mdt check --path packages/lib-b
Or iterate over all directories that contain mdt.toml:
- name: check all mdt projects
run: |
for dir in . packages/*/; do
if [ -f "$dir/mdt.toml" ]; then
echo "Checking $dir"
mdt check --path "$dir"
fi
done
Data isolation
Each sub-project loads its own data files relative to its own mdt.toml. A [data] section in packages/lib-a/mdt.toml resolves paths relative to packages/lib-a/:
# packages/lib-a/mdt.toml
[data]
cargo = "Cargo.toml" # resolves to packages/lib-a/Cargo.toml
package = "package.json" # resolves to packages/lib-a/package.json
This means {{ cargo.package.name }} in lib-a’s templates refers to lib-a’s Cargo.toml, not the root workspace Cargo.toml.