Imported Docs
The Imported Docs plugin enables federated documentation — external repositories can contribute documentation to your main docs site while keeping their docs at source. This plugin supports both push-based (external repos push to you) and pull-based (you pull from external repos) approaches.
It implements IImportHook.OnImportAsync, which runs after content discovery and before navigation filtering. That gives imported pages the same downstream processing as native docs: navigation, preprocessing, rendering, and templates all see the imported pages as if they lived in the site tree from the start.
Quick Start
Minimal Configuration
Enable the plugin in your appsettings.json, and configure it under importedDocs in the
same Netdocs section:
{
"Netdocs": {
"plugins": [
{ "name": "imported-docs" }
],
"importedDocs": {
"pushedDocsDir": "imported"
}
}
}
This enables push-based imports. External repos can push documentation to the /imported directory.
importedDocs is read by the JSON config loader, so a site still running from mkdocs.yml
needs to move to appsettings.json to use this plugin.
Import hook behavior
imported-docs is the built-in plugin that implements IImportHook.OnImportAsync.
- It runs after source docs are discovered.
- It can add pages directly into
site.Pages. - Imported pages then flow through the normal build pipeline.
- Use it for push-based staging, Git pull imports, or S3 pull imports.
Configuration Reference
Top-Level Settings
{
"Netdocs": {
"importedDocs": {
"pushedDocsDir": "imported",
"pullSources": [
// ... pull source definitions
]
}
}
}
| Setting | Type | Default | Description |
|---|---|---|---|
pushedDocsDir |
string | "imported" |
Directory where external repos push docs. Created as a subdirectory in your project root. |
pullSources |
array | [] |
List of external repositories to pull from (see below). |
s3Sources |
array | [] |
List of S3 buckets to pull documentation from (see below). |
Pull Source Configuration
Each pull source is a repository to pull documentation from:
{
"repository": "https://github.com/owner/repo.git",
"reference": "main",
"sourcePath": "docs",
"destinationPath": "external/my-project",
"authTokenEnvVar": "GITHUB_TOKEN",
"includeSourceMarker": true,
"exclude": ["draft/**", "*.tmp"],
"frontMatterDefaults": {
"nav_title": "My Project Docs",
"hide": false
}
}
| Setting | Type | Default | Description |
|---|---|---|---|
repository |
string | (required) | Git repository URL (https or ssh). |
reference |
string | (optional) | Branch, tag, or commit SHA to checkout. If omitted, uses default branch. |
sourcePath |
string | "docs" |
Subdirectory within the repo containing markdown files. |
destinationPath |
string | (required) | Path on main site where imported docs appear (e.g., "products/api" → /products/api/). |
authTokenEnvVar |
string | (optional) | Environment variable containing auth token for private repos. |
includeSourceMarker |
bool | false |
If true, adds import_source and import_url metadata to imported pages. Useful for displaying "view source" links. |
exclude |
array | [] |
Glob patterns for files to exclude (e.g., ["draft/**", "INTERNAL-*.md"]). Supports * (segment) and ** (any dirs). |
frontMatterDefaults |
object | {} |
Front-matter key-value pairs to apply as fallback for imported pages. Extracted values take precedence. |
repoUrl |
string | (derived) | Browsable URL of the source repo for edit/view links, e.g. "https://github.com/org/handbook". Derived from repository when omitted. |
editUri |
string | (derived) | Path appended to repoUrl to reach an editable file, e.g. "edit/main/docs". Derived from the checked-out branch and sourcePath when omitted. |
S3 Source Configuration
Pull documentation directly from S3 buckets (no git clone overhead):
{
"bucket": "my-docs-bucket",
"prefix": "docs/",
"region": "us-east-1",
"destinationPath": "products/external",
"credentialsEnvVar": "AWS_CREDENTIALS",
"includeSourceMarker": true,
"exclude": ["*.draft.md", "private/**"],
"frontMatterDefaults": {
"nav_title": "External Docs"
}
}
| Setting | Type | Default | Description |
|---|---|---|---|
bucket |
string | (required) | S3 bucket name (e.g., "my-docs-bucket"). |
prefix |
string | (required) | S3 object prefix/folder (e.g., "docs/" or "external-docs/api/"). Only objects under this prefix are imported. |
region |
string | (required) | AWS region (e.g., "us-east-1", "eu-west-1"). |
destinationPath |
string | (optional) | Path on main site where imported docs appear (e.g., "products/api"). |
credentialsEnvVar |
string | (optional) | Environment variable containing AWS credentials in format "ACCESS_KEY:SECRET_KEY". If omitted, uses default AWS credential chain (IAM role, ~/.aws/credentials, env vars). |
includeSourceMarker |
bool | false |
If true, adds import_source with S3 URL and import_url metadata. |
exclude |
array | [] |
Glob patterns for files to exclude. |
frontMatterDefaults |
object | {} |
Front-matter key-value pairs to apply as fallback. |
repoUrl |
string | (none) | Browsable URL of the repo backing this bucket, for edit/view links. Without it (and editUri) imported pages show no source buttons. |
editUri |
string | (none) | Path appended to repoUrl to reach an editable file, e.g. "edit/main/docs". |
Edit and view source links
An imported page lives in someone else's repository, but its path in this site is wherever
destinationPath put it. The site-wide repoUrl/editUri would therefore aim the "edit this page"
button at this repo, at a path that only exists upstream. Imported pages resolve their own links
instead:
repoUrl+editUrion the source, if you set them — always wins.- Derived from the clone for pull sources: the repository URL gives the host (an
git@host:org/repo.gitremote is rewritten to itshttps://form), and the branch actually checked out plussourcePathgive the rest. A source that never pinned areferencestill gets correct links this way. - No buttons at all, if neither applies — a repository pinned to a tag or commit (detached, so there is no branch to build a URL around), a remote that is a local path, or an S3 source with nothing configured. A missing button is better than one that 404s.
S3 sources have nothing to derive from, so they need both options to show links.
{
"repository": "git@github.com:org/handbook.git",
"sourcePath": "docs",
"destinationPath": "imported/handbook",
"repoUrl": "https://github.com/org/handbook",
"editUri": "edit/main/docs"
}
Page dates for imported content
git-revision-date reads this repository's history, which knows nothing
about imported files. It no longer falls back to their file timestamps either, since those are
just the moment the import cloned them — which would show every imported page as updated today,
on every build. Imported pages therefore carry no git-derived dates; set them through
frontMatterDefaults if you need them.
Use Cases
Push-Based: External Repo Workflow
Scenario: Your organization has multiple repositories. Each repo maintains its own documentation, and you want it to appear on your main docs site.
Flow:
- External repo has docs in
./docs/directory - External repo GitHub Action triggers on docs changes
- Action clones main docs repo, copies files to
/imported/{project-name}/, commits and pushes - Next build of main docs site picks up the changes
External Repo Workflow Example (.github/workflows/push-docs.yml):
name: Push docs to main site
on:
push:
branches: [main]
paths:
- 'docs/**'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
push-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout this repo
uses: actions/checkout@v4
- name: Checkout docs repo
uses: actions/checkout@v4
with:
repository: myorg/main-docs
token: ${{ secrets.GITHUB_TOKEN }}
path: ./docs-site
- name: Copy docs
run: |
mkdir -p ./docs-site/imported/my-project
cp -r ./docs/* ./docs-site/imported/my-project/
- name: Push to docs repo
working-directory: ./docs-site
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add imported/
git commit -m "docs: push docs from my-project"
git push
Pull-Based: Scheduled Sync
Scenario: You want to automatically pull documentation from multiple external repositories on a schedule, without requiring changes to those repos.
Flow:
- Configure
pullSourcesin main docsappsettings.json - Main docs repo has scheduled GitHub Action
- Action runs build, which pulls from configured external repos
- Imported docs integrated into main site
Main Docs Repo Workflow (.github/workflows/scheduled-build.yml):
name: Scheduled build with external docs
on:
schedule:
# Daily build at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch: # Manual trigger
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '9.0.x'
- name: Build with imported docs
run: |
dotnet build -c Release
dotnet run --project src/Netdocs.Cli -- build
- name: Deploy
run: |
# Your deployment logic here
echo "Deploying built site..."
Main Docs Config (appsettings.json):
{
"Netdocs": {
"plugins": [
{ "name": "imported-docs" }
],
"importedDocs": {
"pushedDocsDir": "imported",
"pullSources": [
{
"repository": "https://github.com/myorg/api-repo.git",
"sourcePath": "docs",
"destinationPath": "products/api",
"exclude": ["README.md", "CONTRIBUTING.md"]
},
{
"repository": "https://github.com/myorg/cli-repo.git",
"sourcePath": "docs",
"destinationPath": "products/cli",
"reference": "v2.x"
}
]
}
}
}
S3-Based: Direct S3 Bucket Import
Scenario: You want to pull documentation directly from S3 buckets without requiring git operations. Faster and simpler for large documentation sets.
Flow:
- External repos upload markdown to a shared S3 bucket (via CI/CD or manually)
- Configure S3 bucket paths in main docs
appsettings.json - Main docs build downloads files directly from S3
- Imported docs integrated into main site
Main Docs Config (appsettings.json):
{
"Netdocs": {
"plugins": [
{ "name": "imported-docs" }
],
"importedDocs": {
"s3Sources": [
{
"bucket": "shared-docs",
"prefix": "api-docs/",
"region": "us-east-1",
"destinationPath": "products/api",
"exclude": ["draft/**", "*.internal.md"]
},
{
"bucket": "shared-docs",
"prefix": "sdk-docs/",
"region": "us-east-1",
"destinationPath": "products/sdk"
}
]
}
}
}
Example External Repo Workflow (to upload to S3):
name: Upload docs to S3
on:
push:
branches: [main]
paths:
- 'docs/**'
env:
AWS_REGION: us-east-1
S3_BUCKET: shared-docs
S3_PREFIX: api-docs/
jobs:
upload:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/GithubActionsRole
aws-region: ${{ env.AWS_REGION }}
- name: Upload to S3
run: |
aws s3 sync ./docs s3://${{ env.S3_BUCKET }}/${{ env.S3_PREFIX }} \
--delete \
--exclude ".git/*" \
--exclude "node_modules/*"
Authentication
Public Repositories
No authentication needed. Simply omit authTokenEnvVar:
{
"repository": "https://github.com/public/repo.git",
"sourcePath": "docs",
"destinationPath": "products/repo"
}
Private Repositories
Use GitHub tokens (or other git credentials) via environment variables.
GitHub Token (Recommended)
In your GitHub Actions workflow, use the default GITHUB_TOKEN:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
In config:
{
"repository": "https://github.com/private/repo.git",
"authTokenEnvVar": "GITHUB_TOKEN",
"sourcePath": "docs",
"destinationPath": "products/repo"
}
The plugin uses the token as OAuth2 credentials (username="oauth2", password=token).
Personal Access Token
Create a Personal Access Token (PAT) with repo scope in GitHub Settings → Developer settings → Personal access tokens.
Store as repository secret (e.g., DOCS_PAT):
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCS_PAT: ${{ secrets.DOCS_PAT }}
Config:
{
"repository": "https://github.com/private/repo.git",
"authTokenEnvVar": "DOCS_PAT",
"sourcePath": "docs",
"destinationPath": "products/repo"
}
SSH Keys
For SSH-based authentication, ensure your CI/CD environment has the SSH private key configured (e.g., via GitHub Secrets + ssh-agent):
- name: Setup SSH
uses: webfactory/ssh-agent@v0.5.4
with:
ssh-private-key: ${{ secrets.DOCS_SSH_KEY }}
Use SSH repository URL:
{
"repository": "git@github.com:private/repo.git",
"sourcePath": "docs",
"destinationPath": "products/repo"
}
No authTokenEnvVar needed — git will use SSH agent.
S3 Bucket Authentication
For S3-based imports, credentials can come from multiple sources (in order of precedence):
- Environment variable (format:
ACCESS_KEY:SECRET_KEY) - IAM role (automatic on AWS EC2, Lambda, or GitHub Actions OIDC)
~/.aws/credentialsfile- Environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY)
Using Environment Variables (Simple)
Store credentials as a GitHub secret and pass via environment:
env:
AWS_CREDENTIALS: ${{ secrets.AWS_CREDENTIALS }}
In config:
{
"bucket": "shared-docs",
"prefix": "docs/",
"region": "us-east-1",
"credentialsEnvVar": "AWS_CREDENTIALS",
"destinationPath": "products/docs"
}
Using IAM Role (Recommended)
For AWS-hosted or OIDC-enabled workflows, use IAM roles instead of credentials:
permissions:
id-token: write
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/GithubActionsRole
aws-region: us-east-1
Then omit credentialsEnvVar in config and the plugin uses the assumed role automatically.
Front-Matter Overrides
The plugin supports merging front-matter from two sources:
- Extracted from imported markdown files (highest priority)
- Config
frontMatterDefaults(fallback)
This allows your main docs site to set default values for imported documentation without overriding values the external repo explicitly set.
Example
External repo's docs/guide.md:
---
title: Getting Started
nav_sort: 10
---
# Getting Started
...
Main docs config:
{
"repository": "https://github.com/external/repo.git",
"sourcePath": "docs",
"destinationPath": "products/external",
"frontMatterDefaults": {
"nav_title": "External Product",
"hide": false,
"nav_sort": 999
}
}
Result: The imported page will have:
title: "Getting Started"← from external markdownnav_title: "External Product"← from config default (external didn't specify)hide: false← from config defaultnav_sort: 10← from external markdown (not overridden)
File Exclusion
Use glob patterns to exclude files from import. Patterns match relative to the source docs path.
Pattern Syntax:
*matches any characters within a path segment (doesn't cross/)**matches any characters across multiple segments?matches a single character{a,b}matches eitheraorb
Examples:
{
"exclude": [
"draft/**", // Exclude entire draft/ directory
"*.tmp", // Exclude .tmp files
"INTERNAL-*.md", // Exclude files starting with INTERNAL-
"**/todo.md", // Exclude todo.md in any directory
"{private,secret}/**" // Exclude private/ and secret/ directories
]
}
Files matching any pattern are skipped during import.
Source Markers
If includeSourceMarker is true, the plugin adds metadata to each imported page:
---
import_source: "https://github.com/external/repo/blob/main/docs/guide.md"
import_url: "/products/external/guide/"
---
This metadata is available in your templates for displaying "View on GitHub" or similar links:
{% if page.frontmatter.import_source %}
<a href="{{ page.frontmatter.import_source }}">View source</a>
{% endif %}
URL Mapping
Imported files are mapped to URLs with the same rules discovered pages use, so an imported tree keeps its shape and its relative cross-links keep resolving.
| Source file | destinationPath |
URL |
|---|---|---|
guide.md |
products/api |
/products/api/guide/ |
integrations/citrix.md |
products/api |
/products/api/integrations/citrix/ |
index.md |
products/api |
/products/api/ |
integrations/index.md |
products/api |
/products/api/integrations/ |
guide.md |
(omitted) | /guide/ |
Behavior:
- The
.mdextension is removed and a trailing slash is added. - Directories below the source path are preserved beneath
destinationPath. index.mdandREADME.mdcollapse onto their containing directory.- When the site sets
slugify.urls, imported segments are slugified too.
Imported pages are also placed in the navigation tree at destinationPath, so they nest
under the surrounding sections rather than at the site root. A .pages file in the matching
directory of your own docs/ tree — docs/products/api/.pages for the examples above —
titles and orders the imported section, even though none of its pages live there.
Build Pipeline Integration
The Imported Docs plugin runs at Stage 2 of the build pipeline — after initial content discovery but before navigation filters and rendering. This ensures:
- Imported docs are discovered early
- All existing plugins can process imported pages
- Navigation generation includes imported docs
- No special handling needed elsewhere
See Build lifecycle for the full pipeline diagram.
Troubleshooting
Pull source fails to clone
Error: Failed to clone repository
Causes:
- Repository URL is incorrect
- Repository is private and
authTokenEnvVaris missing or invalid - Git credentials not configured (SSH key not loaded)
- Network connectivity issue
Solution:
- Verify repository URL
- For private repos, set
authTokenEnvVarand ensure env var is populated - For SSH, verify SSH key is available in CI/CD environment
- Check build logs for details
Files not appearing
Causes:
sourcePathdoesn't exist in the repository- All files match
excludepatterns - Reference (branch/tag) doesn't exist
Solution:
- Verify
sourcePathexists in the repository - Check
excludepatterns with glob tester - Verify
referenceexists on remote
Build succeeds but no docs appear
Check:
- Is the plugin enabled in
pluginsarray? - Does
importedDocssection exist in config? - Check build logs for import activity (look for "Imported X pages" messages)
- Verify
destinationPathis correct and doesn't conflict with existing content
Performance
- Push-based: No performance impact. Only processes files pushed to staging directory.
- Pull-based (Git): Clones repositories to temp directory on each build. For large repos or frequent builds, consider:
- Using scheduled workflows (fewer builds)
- Shallow clones with specific branches/tags
- Filtering large repos with
excludepatterns
- Pull-based (S3): Direct download from S3 bucket without git operations. Generally faster than git clones, especially for large doc sets:
- Parallel object listing for efficient bucket scanning
- Configurable credentials via IAM role or environment variables
- No temporary directory cleanup overhead (streaming downloads)
- Suitable for frequently-updated doc sources
See Also
- Build lifecycle — where the import hook runs
- Events & callbacks — full hook reference
- External plugins — building custom import plugins