private static ModuleList GetModules() => new ModuleList { { GetDocuments, new ModuleCollection { new Documents(Blog.RenderPages), new Concat { new Documents(Blog.Posts) } } }, { GenerateRedirects, new Execute(ctx => { Redirect redirect = new Redirect() .WithMetaRefreshPages(ctx.Bool(BlogKeys.MetaRefreshRedirects)); if (ctx.Bool(BlogKeys.NetlifyRedirects)) { redirect.WithAdditionalOutput("_redirects", redirects => string.Join(Environment.NewLine, redirects.Select(r => $"/{r.Key} {r.Value}"))); } return(redirect); }) }, { WriteFiles, new WriteFiles() } };
private static IModuleList GetModules(RedirectsSettings settings) => new ModuleList { new Documents() .FromPipelines(settings.Pipelines), new Execute(ctx => { Redirect redirect = new Redirect() .WithMetaRefreshPages(settings.MetaRefreshRedirects.Invoke <bool>(ctx)); if (settings.NetlifyRedirects.Invoke <bool>(ctx)) { redirect.WithAdditionalOutput("_redirects", redirects => string.Join(Environment.NewLine, redirects.Select(r => $"/{r.Key} {r.Value}"))); } return(redirect); }), new WriteFiles() };
public void Apply(IEngine engine) { // Global metadata defaults engine.Settings[DocsKeys.SourceFiles] = new [] { "src/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs", "../src/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs" }; engine.Settings[DocsKeys.IncludeGlobalNamespace] = true; engine.Settings[DocsKeys.IncludeDateInPostPath] = false; engine.Settings[DocsKeys.MarkdownExtensions] = "advanced+bootstrap"; engine.Settings[DocsKeys.SearchIndex] = true; engine.Settings[DocsKeys.MetaRefreshRedirects] = true; engine.Settings[DocsKeys.AutoLinkTypes] = true; engine.Settings[DocsKeys.BlogRssPath] = GenerateFeeds.DefaultRssPath; engine.Settings[DocsKeys.BlogAtomPath] = GenerateFeeds.DefaultAtomPath; engine.Settings[DocsKeys.BlogRdfPath] = GenerateFeeds.DefaultRdfPath; engine.Pipelines.Add(DocsPipelines.Code, new ReadFiles(ctx => ctx.List <string>(DocsKeys.SourceFiles)) ); engine.Pipelines.Add(DocsPipelines.Api, new If(ctx => ctx.Documents[DocsPipelines.Code].Any() || ctx.List <string>(DocsKeys.AssemblyFiles)?.Count > 0, new Documents(DocsPipelines.Code), // Put analysis module inside execute to have access to global metadata at runtime new Execute(ctx => new AnalyzeCSharp() .WhereNamespaces(ctx.Bool(DocsKeys.IncludeGlobalNamespace)) .WherePublic() .WithCssClasses("code", "cs") .WithWritePathPrefix("api") .WithAssemblies(ctx.List <string>(DocsKeys.AssemblyFiles)) .WithAssemblySymbols()), // Calculate a type name to link lookup for auto linking new Execute((doc, ctx) => { string name = null; string kind = doc.String(CodeAnalysisKeys.Kind); if (kind == "NamedType") { name = doc.String(CodeAnalysisKeys.DisplayName); } else if (kind == "Property" || kind == "Method") { IDocument containingType = doc.Document(CodeAnalysisKeys.ContainingType); if (containingType != null) { name = $"{containingType.String(CodeAnalysisKeys.DisplayName)}.{doc.String(CodeAnalysisKeys.DisplayName)}"; } } if (name != null) { _typeNamesToLink.AddOrUpdate(name, ctx.GetLink(doc), (x, y) => string.Empty); } }) ) ); engine.Pipelines.Add(DocsPipelines.Pages, new ReadFiles(ctx => $"{{{GetIgnoreFoldersGlob(ctx)}}}/*.md"), new Meta(DocsKeys.EditFilePath, (doc, ctx) => doc.FilePath(Keys.RelativeFilePath)), new Include(), new FrontMatter(new Yaml.Yaml()), new Execute(ctx => new Markdown.Markdown().UseExtensions(ctx.Settings.List <Type>(DocsKeys.MarkdownExternalExtensions)).UseConfiguration(ctx.String(DocsKeys.MarkdownExtensions))), new Concat( // Add any additional Razor pages new ReadFiles(ctx => $"{{{GetIgnoreFoldersGlob(ctx)}}}/{{!_,}}*.cshtml"), new Include(), new FrontMatter(new Yaml.Yaml())), new If(ctx => ctx.Bool(DocsKeys.AutoLinkTypes), new AutoLink(_typeNamesToLink) .WithQuerySelector("code") .WithMatchOnlyWholeWord() ), // This is an ugly hack to re-escape @ symbols in Markdown since AngleSharp unescapes them if it // changes text content to add an auto link, can be removed if AngleSharp #494 is addressed new If((doc, ctx) => doc.String(Keys.SourceFileExt) == ".md", new Replace("@", "@") ), new Excerpt(), new Title(), new WriteFiles(".html").OnlyMetadata(), new Tree() .WithPlaceholderFactory(TreePlaceholderFactory) .WithNesting(true, true) ); engine.Pipelines.Add(DocsPipelines.BlogPosts, new ReadFiles("blog/*.md"), new Meta(DocsKeys.EditFilePath, (doc, ctx) => doc.FilePath(Keys.RelativeFilePath)), new FrontMatter(new Yaml.Yaml()), new Execute(ctx => new Markdown.Markdown().UseExtensions(ctx.Settings.List <Type>(DocsKeys.MarkdownExternalExtensions)).UseConfiguration(ctx.String(DocsKeys.MarkdownExtensions))), new If(ctx => ctx.Bool(DocsKeys.AutoLinkTypes), new AutoLink(_typeNamesToLink) .WithQuerySelector("code") .WithMatchOnlyWholeWord() ), // This is an ugly hack to re-escape @ symbols in Markdown since AngleSharp unescapes them if it // changes text content to add an auto link, can be removed if AngleSharp #494 is addressed new If((doc, ctx) => doc.String(Keys.SourceFileExt) == ".md", new Replace("@", "@") ), new Excerpt(), new Meta("FrontMatterPublished", (doc, ctx) => doc.ContainsKey(DocsKeys.Published)), // Record whether the publish date came from front matter new Meta(DocsKeys.Published, (doc, ctx) => { DateTime published; if (!DateTime.TryParse(doc.String(Keys.SourceFileName).Substring(0, 10), out published)) { Wyam.Common.Tracing.Trace.Warning($"Could not parse published date for {doc.SourceString()}."); return(null); } return(published); }).OnlyIfNonExisting(), new Where((doc, ctx) => { if (!doc.ContainsKey(DocsKeys.Published) || doc.Get(DocsKeys.Published) == null) { Common.Tracing.Trace.Warning($"Skipping {doc.SourceString()} due to not having {DocsKeys.Published} metadata"); return(false); } if (doc.Get <DateTime>(DocsKeys.Published) > DateTime.Now) { Common.Tracing.Trace.Warning($"Skipping {doc.SourceString()} due to having {DocsKeys.Published} metadata in the future of {doc.Get<DateTime>(DocsKeys.Published)} (current date and time is {DateTime.Now})"); return(false); } return(true); }), new Meta(Keys.RelativeFilePath, (doc, ctx) => { DateTime published = doc.Get <DateTime>(DocsKeys.Published); string fileName = doc.Bool("FrontMatterPublished") ? doc.FilePath(Keys.SourceFileName).ChangeExtension("html").FullPath : doc.FilePath(Keys.SourceFileName).ChangeExtension("html").FullPath.Substring(11); return(ctx.Bool(DocsKeys.IncludeDateInPostPath) ? $"blog/{published:yyyy}/{published:MM}/{fileName}" : $"blog/{fileName}"); }), new OrderBy((doc, ctx) => doc.Get <DateTime>(DocsKeys.Published)).Descending() ); engine.Pipelines.Add(DocsPipelines.BlogIndexes, new If(ctx => ctx.Documents[DocsPipelines.BlogPosts].Any(), new ReadFiles("_BlogIndex.cshtml"), new Paginate(5, new Documents(DocsPipelines.BlogPosts), new OrderBy((doc, ctx) => doc.Get <DateTime>(DocsKeys.Published)).Descending() ), new Meta(Keys.Title, (doc, ctx) => doc.Get <int>(Keys.CurrentPage) == 1 ? "Blog" : $"Blog (Page {doc[Keys.CurrentPage]})"), new Meta(Keys.RelativeFilePath, (doc, ctx) => doc.Get <int>(Keys.CurrentPage) == 1 ? "blog/index.html" : $"blog/page{doc[Keys.CurrentPage]}.html"), new Razor.Razor() .IgnorePrefix(null) .WithLayout("/_BlogLayout.cshtml"), new WriteFiles() ) ); engine.Pipelines.Add(DocsPipelines.BlogCategories, new If(ctx => ctx.Documents[DocsPipelines.BlogPosts].Any(), new ReadFiles("_BlogIndex.cshtml"), new GroupBy((doc, ctx) => doc[DocsKeys.Category], new Documents(DocsPipelines.BlogPosts) ), new ForEach( new Paginate(5, new Documents((doc, ctx) => doc[Keys.GroupDocuments]), new OrderBy((doc, ctx) => doc.Get <DateTime>(DocsKeys.Published)).Descending() ), new Meta(Keys.Title, (doc, ctx) => doc.Get <int>(Keys.CurrentPage) == 1 ? $"{doc[Keys.GroupKey]}" : $"{doc[Keys.GroupKey]} (Page {doc[Keys.CurrentPage]})"), new Meta(Keys.RelativeFilePath, (doc, ctx) => { string category = doc.String(Keys.GroupKey).ToLower().Replace(" ", "-").Replace("'", string.Empty); return(doc.Get <int>(Keys.CurrentPage) == 1 ? $"blog/{category}/index.html" : $"blog/{category}/page{doc[Keys.CurrentPage]}.html"); }), new Razor.Razor() .IgnorePrefix(null) .WithLayout("/_BlogLayout.cshtml"), new WriteFiles() ) ) ); engine.Pipelines.Add(DocsPipelines.BlogArchives, new If(ctx => ctx.Documents[DocsPipelines.BlogPosts].Any(), // Monthly archives new ReadFiles("_BlogIndex.cshtml"), new GroupBy((doc, ctx) => new DateTime(doc.Get <DateTime>(DocsKeys.Published).Year, doc.Get <DateTime>(DocsKeys.Published).Month, 1), new Documents(DocsPipelines.BlogPosts) ), new Meta(Keys.Title, (doc, ctx) => doc.Get <DateTime>(Keys.GroupKey).ToString("MMMM, yyyy")), new Meta("Link", (doc, ctx) => doc.Get <DateTime>(Keys.GroupKey).ToString("yyyy/MM")), new Concat( // Yearly archives new ReadFiles("_BlogIndex.cshtml"), new GroupBy((doc, ctx) => new DateTime(doc.Get <DateTime>(DocsKeys.Published).Year, 1, 1), new Documents(DocsPipelines.BlogPosts) ), new Meta(Keys.Title, (doc, ctx) => doc.Get <DateTime>(Keys.GroupKey).ToString("yyyy")), new Meta("Link", (doc, ctx) => doc.Get <DateTime>(Keys.GroupKey).ToString("yyyy")) ), new ForEach( new Paginate(5, new Documents((doc, ctx) => doc[Keys.GroupDocuments]), new OrderBy((doc, ctx) => doc.Get <DateTime>(DocsKeys.Published)).Descending() ), new Meta(Keys.Title, (doc, ctx) => doc.Get <int>(Keys.CurrentPage) == 1 ? $"{doc[Keys.Title]}" : $"{doc[Keys.Title]} (Page {doc[Keys.CurrentPage]})"), new Meta(Keys.RelativeFilePath, (doc, ctx) => doc.Get <int>(Keys.CurrentPage) == 1 ? $"blog/archive/{doc["Link"]}/index.html" : $"blog/archive/{doc["Link"]}/page{doc[Keys.CurrentPage]}.html"), new Razor.Razor() .IgnorePrefix(null) .WithLayout("/_BlogLayout.cshtml"), new WriteFiles() ) ) ); engine.Pipelines.Add(DocsPipelines.BlogAuthors, new If(ctx => ctx.Documents[DocsPipelines.BlogPosts].Any(), new ReadFiles("_BlogIndex.cshtml"), new GroupBy((doc, ctx) => doc[DocsKeys.Author], new Documents(DocsPipelines.BlogPosts) ), new ForEach( new Paginate(5, new Documents((doc, ctx) => doc[Keys.GroupDocuments]), new OrderBy((doc, ctx) => doc.Get <DateTime>(DocsKeys.Published)).Descending() ), new Meta(Keys.Title, (doc, ctx) => doc.Get <int>(Keys.CurrentPage) == 1 ? $"{doc[Keys.GroupKey]}" : $"{doc[Keys.GroupKey]} (Page {doc[Keys.CurrentPage]})"), new Meta(Keys.RelativeFilePath, (doc, ctx) => { string author = doc.String(Keys.GroupKey).ToLower().Replace(" ", "-").Replace("'", string.Empty); return(doc.Get <int>(Keys.CurrentPage) == 1 ? $"blog/author/{author}/index.html" : $"blog/author/{author}/page{doc[Keys.CurrentPage]}.html"); }), new Razor.Razor() .IgnorePrefix(null) .WithLayout("/_BlogLayout.cshtml"), new WriteFiles() ) ) ); engine.Pipelines.Add(DocsPipelines.BlogFeed, new If(ctx => ctx.Documents[DocsPipelines.BlogPosts].Any(), new Documents(DocsPipelines.BlogPosts), new GenerateFeeds() .WithRssPath(ctx => ctx.FilePath(DocsKeys.BlogRssPath)) .WithAtomPath(ctx => ctx.FilePath(DocsKeys.BlogAtomPath)) .WithRdfPath(ctx => ctx.FilePath(DocsKeys.BlogRdfPath)), new WriteFiles() ) ); engine.Pipelines.Add(DocsPipelines.RenderPages, new If(ctx => ctx.Documents[DocsPipelines.Pages].Any(), new Documents(DocsPipelines.Pages), new Flatten(), // Hide the sidebar for root pages if there's no children new Meta(DocsKeys.NoSidebar, (doc, ctx) => doc.Get(DocsKeys.NoSidebar, (doc.DocumentList(Keys.Children)?.Count ?? 0) == 0) && doc.Document(Keys.Parent) == null), new Title(), new Razor.Razor() .WithLayout("/_Layout.cshtml"), new Headings(), new HtmlInsert("div#infobar-headings", (doc, ctx) => ctx.GenerateInfobarHeadings(doc)), new WriteFiles() ) ); // Render the blog after the indexes and archive so the layout doesn't show up when including whole page (I.e., first post) engine.Pipelines.Add(DocsPipelines.RenderBlogPosts, new If(ctx => ctx.Documents[DocsPipelines.BlogPosts].Any(), new Documents(DocsPipelines.BlogPosts), new Razor.Razor() .WithLayout("/_BlogPost.cshtml"), new Headings(), new HtmlInsert("div#infobar-headings", (doc, ctx) => ctx.GenerateInfobarHeadings(doc)), new WriteFiles() ) ); engine.Pipelines.Add(DocsPipelines.Redirects, new Documents(DocsPipelines.RenderPages), new Concat( new Documents(DocsPipelines.RenderBlogPosts) ), new Execute(ctx => { Redirect redirect = new Redirect() .WithMetaRefreshPages(ctx.Bool(DocsKeys.MetaRefreshRedirects)); if (ctx.Bool(DocsKeys.NetlifyRedirects)) { redirect.WithAdditionalOutput("_redirects", redirects => string.Join(Environment.NewLine, redirects.Select(r => $"/{r.Key} {r.Value}"))); } return(redirect); }), new WriteFiles() ); engine.Pipelines.Add(DocsPipelines.RenderApi, new If(ctx => ctx.Documents[DocsPipelines.Api].Any(), new Documents(DocsPipelines.Api), new Razor.Razor() .WithLayout("/_ApiLayout.cshtml"), new Headings(), new HtmlInsert("div#infobar-headings", (doc, ctx) => ctx.GenerateInfobarHeadings(doc)), new WriteFiles() ) ); engine.Pipelines.Add(DocsPipelines.ApiIndex, new If(ctx => ctx.Documents[DocsPipelines.Api].Any(), new ReadFiles("_ApiIndex.cshtml"), new Meta(Keys.RelativeFilePath, "api/index.html"), new Meta(Keys.SourceFileName, "index.html"), new Title("API"), new Razor.Razor(), new WriteFiles() ) ); engine.Pipelines.Add(DocsPipelines.ApiSearchIndex, new If(ctx => ctx.Documents[DocsPipelines.Api].Any() && ctx.Bool(DocsKeys.SearchIndex), new Documents(DocsPipelines.Api), new Where((doc, ctx) => doc.String(CodeAnalysisKeys.Kind) == "NamedType"), new SearchIndex.SearchIndex((doc, ctx) => new SearchIndexItem( ctx.GetLink(doc), doc.String(CodeAnalysisKeys.DisplayName), doc.String(CodeAnalysisKeys.DisplayName) )) .WithScript((scriptBuilder, context) => { // Use a custom tokenizer that splits on camel case characters // https://github.com/olivernn/lunr.js/issues/230#issuecomment-244790648 scriptBuilder.Insert(0, @" var camelCaseTokenizer = function (obj) { var previous = ''; return obj.toString().trim().split(/[\s\-]+|(?=[A-Z])/).reduce(function(acc, cur) { var current = cur.toLowerCase(); if(acc.length === 0) { previous = current; return acc.concat(current); } previous = previous.concat(current); return acc.concat([current, previous]); }, []); } lunr.tokenizer.registerFunction(camelCaseTokenizer, 'camelCaseTokenizer')"); scriptBuilder.Replace("this.ref('id');", @"this.ref('id'); this.tokenizer(camelCaseTokenizer);"); return(scriptBuilder.ToString()); }) .WithPath("assets/js/searchIndex.js"), new WriteFiles() ) ); engine.Pipelines.Add(DocsPipelines.Less, new ReadFiles("assets/css/*.less"), new Concat( new ReadFiles("assets/css/bootstrap/bootstrap.less") ), new Concat( new ReadFiles("assets/css/adminlte/AdminLTE.less") ), new Concat( new ReadFiles("assets/css/theme/theme.less") ), new Less.Less(), new WriteFiles(".css") ); engine.Pipelines.Add(DocsPipelines.Resources, new CopyFiles("**/*{!.cshtml,!.md,!.less,}") ); engine.Pipelines.Add(DocsPipelines.ValidateLinks, new If(ctx => ctx.Bool(DocsKeys.ValidateAbsoluteLinks) || ctx.Bool(DocsKeys.ValidateRelativeLinks), new Documents(DocsPipelines.RenderPages), new Concat( new Documents(DocsPipelines.RenderBlogPosts) ), new Concat( new Documents(DocsPipelines.RenderApi) ), new Concat( new Documents(DocsPipelines.Resources) ), new Where((doc, ctx) => { FilePath destinationPath = doc.FilePath(Keys.DestinationFilePath); return(destinationPath != null && (destinationPath.Extension == ".html" || destinationPath.Extension == ".htm")); }), new Execute(ctx => new ValidateLinks() .ValidateAbsoluteLinks(ctx.Bool(DocsKeys.ValidateAbsoluteLinks)) .ValidateRelativeLinks(ctx.Bool(DocsKeys.ValidateRelativeLinks)) .AsError(ctx.Bool(DocsKeys.ValidateLinksAsError) ) ) ) ); }
public void Apply(IEngine engine) { // Global metadata defaults engine.GlobalMetadata[BlogKeys.Title] = "My Blog"; engine.GlobalMetadata[BlogKeys.Description] = "Welcome!"; engine.GlobalMetadata[BlogKeys.MarkdownExtensions] = "advanced+bootstrap"; engine.GlobalMetadata[BlogKeys.IncludeDateInPostPath] = false; engine.GlobalMetadata[BlogKeys.PostsPath] = new DirectoryPath("posts"); engine.GlobalMetadata[BlogKeys.MetaRefreshRedirects] = true; // Get the pages first so they're available in the navbar, but don't render until last engine.Pipelines.Add(BlogPipelines.Pages, new ReadFiles(ctx => $"{{!{ctx.DirectoryPath(BlogKeys.PostsPath).FullPath},**}}/*.md"), new FrontMatter(new Yaml.Yaml()), new Execute(ctx => new Markdown.Markdown().UseConfiguration(ctx.String(BlogKeys.MarkdownExtensions))), new Concat( // Add any additional Razor pages new ReadFiles(ctx => $"{{!{ctx.DirectoryPath(BlogKeys.PostsPath).FullPath},!tags,**}}/*.cshtml"), new FrontMatter(new Yaml.Yaml()) ), new Concat( // Add the posts index page new ReadFiles("posts/index.cshtml"), new FrontMatter(new Yaml.Yaml()), new Meta(Keys.RelativeFilePath, ctx => ctx.DirectoryPath(BlogKeys.PostsPath).CombineFile("index.cshtml")) ), new Concat( // Add the tags index page new ReadFiles("tags/index.cshtml"), new FrontMatter(new Yaml.Yaml()) ), // Copy the index page image and header text color from global metadata (if there is one) new If((doc, ctx) => doc.FilePath(Keys.RelativeFilePath).Equals(new FilePath("index.cshtml")) && ctx.ContainsKey(BlogKeys.Image), new Meta(BlogKeys.Image, ctx => ctx[BlogKeys.Image])), new If((doc, ctx) => doc.FilePath(Keys.RelativeFilePath).Equals(new FilePath("index.cshtml")) && ctx.ContainsKey(BlogKeys.HeaderTextColor), new Meta(BlogKeys.HeaderTextColor, ctx => ctx[BlogKeys.HeaderTextColor])), new WriteFiles(".html") .OnlyMetadata() ); engine.Pipelines.Add(BlogPipelines.RawPosts, new ReadFiles(ctx => $"{ctx.DirectoryPath(BlogKeys.PostsPath).FullPath}/*.md"), new FrontMatter(new Yaml.Yaml()), new Execute(ctx => new Markdown.Markdown().UseConfiguration(ctx.String(BlogKeys.MarkdownExtensions))), new Concat( // Add any posts written in Razor new ReadFiles(ctx => $"{ctx.DirectoryPath(BlogKeys.PostsPath).FullPath}/{{!_,!index,}}*.cshtml"), new FrontMatter(new Yaml.Yaml())), new Meta("FrontMatterPublished", (doc, ctx) => doc.ContainsKey(BlogKeys.Published)), // Record whether the publish date came from front matter new Meta(BlogKeys.Published, (doc, ctx) => DateTime.Parse(doc.String(Keys.SourceFileName).Substring(0, 10))).OnlyIfNonExisting(), new Where((doc, ctx) => doc.ContainsKey(BlogKeys.Published) && doc.Get <DateTime>(BlogKeys.Published) <= DateTime.Today), new Meta(Keys.RelativeFilePath, (doc, ctx) => { DateTime published = doc.Get <DateTime>(BlogKeys.Published); string fileName = doc.Get <bool>("FrontMatterPublished") ? doc.FilePath(Keys.SourceFileName).ChangeExtension("html").FullPath : doc.FilePath(Keys.SourceFileName).ChangeExtension("html").FullPath.Substring(11); return(ctx.Get <bool>(BlogKeys.IncludeDateInPostPath) ? $"{ctx.DirectoryPath(BlogKeys.PostsPath).FullPath}/{published:yyyy}/{published:MM}/{fileName}" : $"{ctx.DirectoryPath(BlogKeys.PostsPath).FullPath}/{fileName}"); }), new OrderBy((doc, ctx) => doc.Get <DateTime>(BlogKeys.Published)).Descending() ); engine.Pipelines.Add(BlogPipelines.Tags, new ReadFiles("tags/tag.cshtml"), new FrontMatter(new Yaml.Yaml()), new Execute(ctx => new GroupByMany(BlogKeys.Tags, new Documents(BlogPipelines.RawPosts)) .WithComparer(ctx.Get <bool>(BlogKeys.CaseInsensitiveTags) ? StringComparer.OrdinalIgnoreCase : null)), new Where((doc, ctx) => !string.IsNullOrEmpty(doc.String(Keys.GroupKey))), new Meta(BlogKeys.Tag, (doc, ctx) => doc.String(Keys.GroupKey)), new Meta(BlogKeys.Title, (doc, ctx) => doc.String(Keys.GroupKey)), new Meta(BlogKeys.Posts, (doc, ctx) => doc.List <IDocument>(Keys.GroupDocuments)), new Meta(Keys.RelativeFilePath, (doc, ctx) => { string tag = doc.String(Keys.GroupKey); return($"tags/{(tag.StartsWith(".") ? tag.Substring(1) : tag).ToLowerInvariant().Replace(' ', '-')}.html"); }), new Razor.Razor() .WithLayout("/_Layout.cshtml"), new WriteFiles() ); // Defer rendering the posts until after the tags have been generated engine.Pipelines.Add(BlogPipelines.Posts, new Documents(BlogPipelines.RawPosts), new Razor.Razor() .WithLayout("/_PostLayout.cshtml"), new Excerpt() .WithMetadataKey(BlogKeys.Excerpt), new Excerpt("div#content") .WithMetadataKey(BlogKeys.Content) .WithOuterHtml(false), new WriteFiles(".html"), // Order them again since the order would have gotten messed up by the concurrent Razor rendering new OrderBy((doc, ctx) => doc.Get <DateTime>(BlogKeys.Published)).Descending()); engine.Pipelines.Add(BlogPipelines.Feed, new Documents(BlogPipelines.Posts), new GenerateFeeds() .WithRssPath(ctx => ctx.FilePath(BlogKeys.RssPath)) .WithAtomPath(ctx => ctx.FilePath(BlogKeys.AtomPath)) .WithRdfPath(ctx => ctx.FilePath(BlogKeys.RdfPath)), new WriteFiles()); engine.Pipelines.Add(BlogPipelines.RenderPages, new Documents(BlogPipelines.Pages), new Razor.Razor() .WithLayout("/_Layout.cshtml"), new WriteFiles() ); engine.Pipelines.Add(BlogPipelines.Redirects, new Documents(BlogPipelines.RenderPages), new Concat( new Documents(BlogPipelines.Posts) ), new Execute(ctx => { Redirect redirect = new Redirect() .WithMetaRefreshPages(ctx.Get <bool>(BlogKeys.MetaRefreshRedirects)); if (ctx.Get <bool>(BlogKeys.NetlifyRedirects)) { redirect.WithAdditionalOutput("_redirects", redirects => string.Join(Environment.NewLine, redirects.Select(r => $"/{r.Key} {r.Value}"))); } return(redirect); }), new WriteFiles() ); engine.Pipelines.Add(BlogPipelines.Resources, new CopyFiles("**/*{!.cshtml,!.md,}") ); }