MarkdownContentService
Pennington.Content
Discovers and provides markdown content from a directory. When LocalizationOptions has multiple locales, also discovers content from locale subdirectories (e.g., Content/fr/, Content/de/).
Properties
AbsoluteContentRootstring- Absolute filesystem path to the root of this service's content directory.
BasePageUrlUrlPath- URL prefix prepended to routes generated from this content directory.
DefaultSectionLabelstring- Default section label applied to discovered items that do not supply one via front matter.
ExcludePathsImmutableArray<string>- Normalized forward-slash subtree paths (relative to the content root) that are skipped during discovery.
SearchPriorityint- Relative priority for ordering results in the search index (higher values rank first).
WatchScopesIReadOnlyList<FileWatchScope>- Directories needing an OS-level watcher. Empty (the default) for aggregators that ride notifications other watchers already produce.
Constructors
MarkdownContentService
#public MarkdownContentService`1(MarkdownContentServiceOptions options, FrontMatterParser parser, IFileSystem fileSystem, LocalizationOptions localization, TimeProvider clock = null, ILogger<MarkdownContentService<TFrontMatter>> logger = null)
Initializes the service and prepares lazy metadata loading. FileWatchDispatcher watches the content directory and drives cache invalidation through OnFileChanged.
Parameters
optionsMarkdownContentServiceOptionsparserFrontMatterParserfileSystemIFileSystemlocalizationLocalizationOptionsclockTimeProviderloggerILogger<MarkdownContentService<TFrontMatter>>
Fields
FolderMetadataSidecarFileNamestring- Default:
"_meta.yml"Sidecar filename that, when dropped at any folder under the content root, declares folder metadata (display title, sort order, llms-subtree opt-in). LlmsOnlyFileSuffixstring- Default:
".llms.md"File-name suffix that marks a markdown file as llms-only — emitted to the llms.txt sidecar but never as an HTML page. NotFoundPageFileNamestring- Default:
"404.md"Content-root file name reserved as the not-found page body whenReserveNotFoundPageis set.
Methods
DiscoverAsync
#public IAsyncEnumerable<DiscoveredItem> DiscoverAsync()
Discover all content items this service is responsible for.
Returns
IAsyncEnumerable<DiscoveredItem>GetAffectedRoutes
#public ContentChangeImpact GetAffectedRoutes(FileChangeNotification change)
Maps a file-change notification to the set of routes this service projects from that file, without mutating any cached state. Consulted by file-watched caches (SiteProjection, BuildHtmlCache) to invalidate only the affected entries instead of clearing wholesale. Default: None.
Parameters
changeFileChangeNotification
Returns
ContentChangeImpactGetContentTocEntriesAsync
#public Task<ImmutableList<ContentTocItem>> GetContentTocEntriesAsync()
Navigation entries for table of contents.
Returns
Task<ImmutableList<ContentTocItem>>GetContentToCopyAsync
#public Task<ImmutableList<ContentToCopy>> GetContentToCopyAsync()
Static files to copy to output (images, downloads, etc.)
Returns
Task<ImmutableList<ContentToCopy>>GetCrossReferencesAsync
#public Task<ImmutableList<CrossReference>> GetCrossReferencesAsync()
Cross-references for xref resolution.
Returns
Task<ImmutableList<CrossReference>>GetFolderMetadataAsync
#public Task<ImmutableList<FolderMetadata>> GetFolderMetadataAsync()
Returns the folder-metadata rows declared by this provider's content.
Returns
Task<ImmutableList<FolderMetadata>>GetIndexableEntriesAsync
#public Task<ImmutableList<ContentTocItem>> GetIndexableEntriesAsync()
Entries that should appear in the search index and llms.txt.
Default: returns GetContentTocEntriesAsync. That is correct when "shown in navigation" ≡ "discoverable via search" — the default holds for markdown, because MarkdownContentService's TOC entries already honor search: and llms: front-matter fields via ExcludeFromSearch / ExcludeFromLlms.
Override when the two sets diverge — for example, RazorPageContentService emits sidecar-less pages here (so users can search for them) without adding them to navigation (which would clutter the TOC with auto-titled entries).
Implementors of custom content services that build ContentTocItems directly: set ExcludeFromSearch and ExcludeFromLlms from your metadata's Search / Llms flags, or per-page opt-outs will be silently ignored.
Returns
Task<ImmutableList<ContentTocItem>>GetLlmsSubtreesAsync
#public Task<ImmutableList<LlmsSubtree>> GetLlmsSubtreesAsync()
Returns the subtrees declared by this provider's content.
Returns
Task<ImmutableList<LlmsSubtree>>GetRecordsAsync
#public IAsyncEnumerable<ContentRecord> GetRecordsAsync()
Projects this service's routable content as ContentRecords — the discovery seam consumed by taxonomy, search faceting, and structured-data emission.
Default: bridges from DiscoverAsync, yielding one record per discovered item that carries Metadata and is neither a RedirectSource (transport, not content) nor an LlmsOnlySource (no human-facing URL). A service that attaches typed metadata to its discovered items — as MarkdownContentService does — therefore participates with no extra code. Override only to project records that do not flow through DiscoverAsync, or to suppress records entirely.
A service that emits routable content from DiscoverAsync but leaves Metadata unset projects no records, and so silently sits out of taxonomy, search faceting, and structured data. Set the metadata (or override this) to opt in.
Returns
IAsyncEnumerable<ContentRecord>GetRedirectSourcesAsync
#public Task<ImmutableList<DiscoveredItem>> GetRedirectSourcesAsync()
Redirect sources this service emits (each item's Source is a RedirectSource). Consumed by RedirectContentService to build the unified redirect map without iterating every service's DiscoverAsync — which would force services that have no redirects to pay the full cost of discovery just to return nothing. Default: empty. Services backed by front-matter records that implement IRedirectable override this.
Returns
Task<ImmutableList<DiscoveredItem>>OnFileChanged
#public FileWatchResponse OnFileChanged(FileChangeNotification change)
Surgically updates the discovery caches when a file under this source's content directory changes. A markdown edit re-parses just that file's front matter; a _meta.yml change re-reads just that sidecar; an asset change is a no-op. Renames (where the watcher only carries the new path) fall back to a full re-seed. Changes outside the scope are ignored.
Parameters
changeFileChangeNotification
Returns
FileWatchResponseParseContentAsync
#public IAsyncEnumerable<ParsedItem> ParseContentAsync()
Discovers and parses this service's content with the service's own front-matter type, yielding ParsedItems (typed metadata + body). Consumers like LlmsTxtService use this instead of re-parsing with a foreign parser, which would mis-flag valid keys from other content types. Default: empty — services whose content is sourced elsewhere (Razor/API pages fetched as rendered HTML) opt out.
Returns
IAsyncEnumerable<ParsedItem>Pennington.Content.MarkdownContentService
namespace Pennington.Content;
/// Discovers and provides markdown content from a directory. When LocalizationOptions has multiple locales, also discovers content from locale subdirectories (e.g., Content/fr/, Content/de/).
public class MarkdownContentService
{
/// Absolute filesystem path to the root of this service's content directory.
public string AbsoluteContentRoot { get; }
/// URL prefix prepended to routes generated from this content directory.
public UrlPath BasePageUrl { get; }
/// Default section label applied to discovered items that do not supply one via front matter.
public string DefaultSectionLabel { get; }
/// Discover all content items this service is responsible for.
public IAsyncEnumerable<DiscoveredItem> DiscoverAsync()
;
/// Normalized forward-slash subtree paths (relative to the content root) that are skipped during discovery.
public ImmutableArray<string> ExcludePaths { get; }
/// Sidecar filename that, when dropped at any folder under the content root, declares folder metadata (display title, sort order, llms-subtree opt-in).
public static const string FolderMetadataSidecarFileName
;
/// Maps a file-change notification to the set of routes this service projects from that file, without mutating any cached state. Consulted by file-watched caches (SiteProjection, BuildHtmlCache) to invalidate only the affected entries instead of clearing wholesale. Default: None.
public ContentChangeImpact GetAffectedRoutes(FileChangeNotification change)
;
/// Navigation entries for table of contents.
public Task<ImmutableList<ContentTocItem>> GetContentTocEntriesAsync()
;
/// Static files to copy to output (images, downloads, etc.)
public Task<ImmutableList<ContentToCopy>> GetContentToCopyAsync()
;
/// Cross-references for xref resolution.
public Task<ImmutableList<CrossReference>> GetCrossReferencesAsync()
;
/// Returns the folder-metadata rows declared by this provider's content.
public Task<ImmutableList<FolderMetadata>> GetFolderMetadataAsync()
;
/// Entries that should appear in the search index and llms.txt. Default: returns GetContentTocEntriesAsync. That is correct when "shown in navigation" ≡ "discoverable via search" — the default holds for markdown, because MarkdownContentService's TOC entries already honor search: and llms: front-matter fields via ExcludeFromSearch / ExcludeFromLlms.Override when the two sets diverge — for example, RazorPageContentService emits sidecar-less pages here (so users can search for them) without adding them to navigation (which would clutter the TOC with auto-titled entries).Implementors of custom content services that build ContentTocItems directly: set ExcludeFromSearch and ExcludeFromLlms from your metadata's Search / Llms flags, or per-page opt-outs will be silently ignored.
public Task<ImmutableList<ContentTocItem>> GetIndexableEntriesAsync()
;
/// Returns the subtrees declared by this provider's content.
public Task<ImmutableList<LlmsSubtree>> GetLlmsSubtreesAsync()
;
/// Projects this service's routable content as ContentRecords — the discovery seam consumed by taxonomy, search faceting, and structured-data emission. Default: bridges from DiscoverAsync, yielding one record per discovered item that carries Metadata and is neither a RedirectSource (transport, not content) nor an LlmsOnlySource (no human-facing URL). A service that attaches typed metadata to its discovered items — as MarkdownContentService does — therefore participates with no extra code. Override only to project records that do not flow through DiscoverAsync, or to suppress records entirely.A service that emits routable content from DiscoverAsync but leaves Metadata unset projects no records, and so silently sits out of taxonomy, search faceting, and structured data. Set the metadata (or override this) to opt in.
public IAsyncEnumerable<ContentRecord> GetRecordsAsync()
;
/// Redirect sources this service emits (each item's Source is a RedirectSource). Consumed by RedirectContentService to build the unified redirect map without iterating every service's DiscoverAsync — which would force services that have no redirects to pay the full cost of discovery just to return nothing. Default: empty. Services backed by front-matter records that implement IRedirectable override this.
public Task<ImmutableList<DiscoveredItem>> GetRedirectSourcesAsync()
;
/// File-name suffix that marks a markdown file as llms-only — emitted to the llms.txt sidecar but never as an HTML page.
public static const string LlmsOnlyFileSuffix
;
/// Initializes the service and prepares lazy metadata loading. FileWatchDispatcher watches the content directory and drives cache invalidation through OnFileChanged.
public MarkdownContentService`1(MarkdownContentServiceOptions options, FrontMatterParser parser, IFileSystem fileSystem, LocalizationOptions localization, TimeProvider clock = null, ILogger<MarkdownContentService<TFrontMatter>> logger = null)
;
/// Content-root file name reserved as the not-found page body when ReserveNotFoundPage is set.
public static const string NotFoundPageFileName
;
/// Surgically updates the discovery caches when a file under this source's content directory changes. A markdown edit re-parses just that file's front matter; a _meta.yml change re-reads just that sidecar; an asset change is a no-op. Renames (where the watcher only carries the new path) fall back to a full re-seed. Changes outside the scope are ignored.
public FileWatchResponse OnFileChanged(FileChangeNotification change)
;
/// Discovers and parses this service's content with the service's own front-matter type, yielding ParsedItems (typed metadata + body). Consumers like LlmsTxtService use this instead of re-parsing with a foreign parser, which would mis-flag valid keys from other content types. Default: empty — services whose content is sourced elsewhere (Razor/API pages fetched as rendered HTML) opt out.
public IAsyncEnumerable<ParsedItem> ParseContentAsync()
;
/// Relative priority for ordering results in the search index (higher values rank first).
public int SearchPriority { get; }
/// Directories needing an OS-level watcher. Empty (the default) for aggregators that ride notifications other watchers already produce.
public IReadOnlyList<FileWatchScope> WatchScopes { get; }
}