Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions .github/workflows/examples/appsettings.imported-docs.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// with push-based, pull-based (git), and S3-based documentation sources

{
"siteConfig": {
"Netdocs": {
// ... other site config ...

"importedDocs": {
Expand Down Expand Up @@ -61,15 +61,15 @@
}
}
]
}
},

// Plugin configuration
"plugins": [
// ... other plugins ...
{
"name": "imported-docs"
// No options needed; configure via siteConfig.importedDocs
}
]
},
// Plugin configuration
"plugins": [
// ... other plugins ...
{
"name": "imported-docs"
// No options needed; configure via the importedDocs section above
}
]
}
}
57 changes: 37 additions & 20 deletions docs-site/docs/plugins/imported-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ It implements `IImportHook.OnImportAsync`, which runs after content discovery an

### Minimal Configuration

Enable the plugin in your `appsettings.json`:
Enable the plugin in your `appsettings.json`, and configure it under `importedDocs` in the
same `Netdocs` section:

```json
{
"plugins": [
{ "name": "imported-docs" }
],
"siteConfig": {
"Netdocs": {
"plugins": [
{ "name": "imported-docs" }
],
"importedDocs": {
"pushedDocsDir": "imported"
}
Expand All @@ -29,6 +30,9 @@ Enable the plugin in your `appsettings.json`:

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`.
Expand All @@ -44,7 +48,7 @@ This enables push-based imports. External repos can push documentation to the `/

```json
{
"siteConfig": {
"Netdocs": {
"importedDocs": {
"pushedDocsDir": "imported",
"pullSources": [
Expand Down Expand Up @@ -228,10 +232,10 @@ jobs:

```json
{
"plugins": [
{ "name": "imported-docs" }
],
"siteConfig": {
"Netdocs": {
"plugins": [
{ "name": "imported-docs" }
],
"importedDocs": {
"pushedDocsDir": "imported",
"pullSources": [
Expand Down Expand Up @@ -267,10 +271,10 @@ jobs:

```json
{
"plugins": [
{ "name": "imported-docs" }
],
"siteConfig": {
"Netdocs": {
"plugins": [
{ "name": "imported-docs" }
],
"importedDocs": {
"s3Sources": [
{
Expand Down Expand Up @@ -556,15 +560,28 @@ This metadata is available in your templates for displaying "View on GitHub" or

## URL Mapping

Imported files are mapped to URLs following Netdocs conventions:
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.

- File: `docs/guide.md` with `destinationPath: "products/api"`
- URL: `/products/api/guide/`
| 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:
- `.md` extension is removed
- Trailing slash always added
- Destination is applied at directory level

- The `.md` extension is removed and a trailing slash is added.
- Directories below the source path are preserved beneath `destinationPath`.
- `index.md` and `README.md` collapse 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

Expand Down
41 changes: 41 additions & 0 deletions src/Netdocs.Core/Configuration/JsonConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,50 @@ public static SiteConfig Load(string appSettingsPath)
Deploy = ParseDeploy(root.Get("deploy").AsMap()),
Optimize = ParseOptimize(root.Get("optimize").AsMap()),
Validation = ParseValidation(root.Get("validation").AsMap()),
ImportedDocs = ParseImportedDocs(root.Get("importedDocs").AsMap()),
};
}

private static ImportedDocsConfig ParseImportedDocs(IReadOnlyDictionary<string, object?> m) => new()
{
PushedDocsDir = m.Get("pushedDocsDir").AsString(),
PullSources = [.. m.Get("pullSources").AsList().Select(x => ParsePullSource(x.AsMap()))],
S3Sources = [.. m.Get("s3Sources").AsList().Select(x => ParseS3Source(x.AsMap()))],
};

private static ImportedDocsPullSource ParsePullSource(IReadOnlyDictionary<string, object?> m) => new()
{
Repository = Required(m, "repository", "importedDocs.pullSources"),
Reference = m.Get("reference").AsString(),
SourcePath = m.Get("sourcePath").AsString() ?? "docs",
DestinationPath = m.Get("destinationPath").AsString(),
AuthTokenEnvVar = m.Get("authTokenEnvVar").AsString(),
ScheduleCron = m.Get("scheduleCron").AsString(),
IncludeSourceMarker = m.Get("includeSourceMarker").AsBool(false),
Exclude = StringList(m.Get("exclude")),
FrontMatterDefaults = m.Get("frontMatterDefaults").AsMap(),
};

private static ImportedDocsS3Source ParseS3Source(IReadOnlyDictionary<string, object?> m) => new()
{
Bucket = Required(m, "bucket", "importedDocs.s3Sources"),
Prefix = Required(m, "prefix", "importedDocs.s3Sources"),
Region = Required(m, "region", "importedDocs.s3Sources"),
DestinationPath = m.Get("destinationPath").AsString(),
CredentialsEnvVar = m.Get("credentialsEnvVar").AsString(),
IncludeSourceMarker = m.Get("includeSourceMarker").AsBool(false),
Exclude = StringList(m.Get("exclude")),
FrontMatterDefaults = m.Get("frontMatterDefaults").AsMap(),
};

private static string Required(IReadOnlyDictionary<string, object?> m, string key, string section)
{
var value = m.Get(key).AsString();
return string.IsNullOrWhiteSpace(value)
? throw new InvalidOperationException($"Each entry in '{section}' requires a non-empty '{key}'.")
: value;
}

private static ValidationConfig ParseValidation(IReadOnlyDictionary<string, object?> m) => new()
{
Links = m.Get("links").AsBool(false),
Expand Down
Loading