Compare commits
1 Commits
codeHighli
...
rsslinks
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17d69ac124 |
@@ -12,13 +12,38 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
private readonly PostCache postCache;
|
||||
|
||||
public HomeController(PostCache postCache)
|
||||
public static List<IPost> postsAsList = new BlogFactory().GetAllPosts().OrderByDescending(a=>a.PublishDate).ToList();
|
||||
public static Dictionary<string, List<IPost>> tagToPost = postsAsList.Where(a=>a.tags != null)
|
||||
.Aggregate(
|
||||
new Dictionary<string, List<IPost>>(),
|
||||
(accum, item) => {
|
||||
foreach(var tag in item.tags)
|
||||
{
|
||||
if(accum.TryGetValue(tag, out var list))
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
accum[tag] = new List<IPost>() { item };
|
||||
}
|
||||
}
|
||||
return accum;
|
||||
});
|
||||
public static IDictionary<string, IPost> posts = postsAsList.ToDictionary(a => a.Url);
|
||||
public static IDictionary<int, List<IPost>> postsByPage = postsAsList.Aggregate(new Dictionary<int, List<IPost>>() { [1] = new List<IPost>() }, (accum, item) =>
|
||||
{
|
||||
this.postCache = postCache;
|
||||
}
|
||||
|
||||
var highestPage = accum.Keys.Max();
|
||||
var current = accum[highestPage].Count;
|
||||
if (current >= 10)
|
||||
{
|
||||
accum[highestPage + 1] = new List<IPost>() { item };
|
||||
return accum;
|
||||
}
|
||||
accum[highestPage].Add(item);
|
||||
return accum;
|
||||
});
|
||||
|
||||
[Route("/")]
|
||||
[Route("/index.html")]
|
||||
[Route("/page/{pageNumber?}" )]
|
||||
@@ -26,11 +51,11 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
[ResponseCache(Duration = 900)]
|
||||
public IActionResult Index(int pageNumber = 1)
|
||||
{
|
||||
if(!postCache.PostsByPage.TryGetValue(pageNumber, out var result))
|
||||
if(!postsByPage.TryGetValue(pageNumber, out var result))
|
||||
{
|
||||
return Redirect($"/404/?from=/page/{pageNumber}/");
|
||||
return Redirect("/404/");
|
||||
}
|
||||
return View(new HomeViewModel() { Posts = result, Page = pageNumber, HasNext = postCache.PostsByPage.ContainsKey(pageNumber + 1), HasPrevious = postCache.PostsByPage.ContainsKey(pageNumber - 1) });
|
||||
return View(new HomeViewModel() { Posts = result, Page = pageNumber, HasNext = postsByPage.ContainsKey(pageNumber + 1), HasPrevious = postsByPage.ContainsKey(pageNumber - 1) });
|
||||
}
|
||||
[Route("/theme/{postName?}")]
|
||||
public IActionResult Theme(string postName)
|
||||
@@ -56,9 +81,9 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
[ResponseCache(Duration = 900)]
|
||||
public IActionResult Post(string postUrl)
|
||||
{
|
||||
if(!postCache.UrlToPost.TryGetValue(postUrl, out var currentPost))
|
||||
if(!posts.TryGetValue(postUrl, out var currentPost))
|
||||
{
|
||||
return Redirect($"/404/?from={postUrl}");
|
||||
return Redirect("/404/");
|
||||
}
|
||||
return View(model: currentPost);
|
||||
}
|
||||
@@ -72,10 +97,10 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
[Route("/404")]
|
||||
[Route("{*url}", Order = 999)]
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult FourOhFour(string from = null)
|
||||
public IActionResult FourOhFour()
|
||||
{
|
||||
this.Response.StatusCode = 404;
|
||||
return View(viewName: nameof(FourOhFour));
|
||||
return View();
|
||||
}
|
||||
[Route("/404.html")]
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
|
||||
@@ -16,14 +16,13 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
public class SeoController : Controller
|
||||
{
|
||||
private readonly BlogConfiguration configuration;
|
||||
private readonly PostCache postCache;
|
||||
|
||||
public SeoController(BlogConfiguration configuration, PostCache postCache)
|
||||
public SeoController(BlogConfiguration configuration)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.postCache = postCache;
|
||||
|
||||
}
|
||||
public static DateTimeOffset publishDate = DateTimeOffset.UtcNow; // keep publish date in memory so we just return when the server was kicked
|
||||
public static IEnumerable<SyndicationItem> postsToSyndication = HomeController.postsAsList.Select(a => a.ToSyndicationItem()).ToList();
|
||||
[Route("/rss")]
|
||||
[Route("/rss.xml")]
|
||||
[ResponseCache(Duration = 7200)]
|
||||
@@ -39,7 +38,7 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
await writer.WriteValue("link", configuration.Link);
|
||||
await writer.WriteDescription("My name is Tommy Parnell. I usually go by TerribleDev on the internets. These are just some of my writings and rants about the software space.");
|
||||
|
||||
foreach (var item in postCache.PostsAsSyndication)
|
||||
foreach (var item in postsToSyndication)
|
||||
{
|
||||
await writer.Write(item);
|
||||
}
|
||||
@@ -55,14 +54,14 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
{
|
||||
Response.StatusCode = 200;
|
||||
Response.ContentType = "text/xml";
|
||||
var sitewideLinks = new List<SiteMapItem>(postCache.TagsToPosts.Keys.Select(a => new SiteMapItem() { LastModified = DateTime.UtcNow, Location = $"https://blog.terribledev.io/tag/{a}/" }))
|
||||
var sitewideLinks = new List<SiteMapItem>(HomeController.tagToPost.Keys.Select(a => new SiteMapItem() { LastModified = DateTime.UtcNow, Location = $"https://blog.terribledev.io/tag/{a}/" }))
|
||||
{
|
||||
new SiteMapItem() { LastModified = DateTime.UtcNow, Location="https://blog.terribledev.io/all-tags/" }
|
||||
};
|
||||
var ser = new XmlSerializer(typeof(SiteMapRoot));
|
||||
var sitemap = new SiteMapRoot()
|
||||
{
|
||||
Urls = postCache.PostsAsLists.Select(a => new SiteMapItem() { LastModified = DateTime.UtcNow, Location = $"https://blog.terribledev.io/{a.Url}/" }).ToList()
|
||||
Urls = HomeController.postsAsList.Select(a => new SiteMapItem() { LastModified = DateTime.UtcNow, Location = $"https://blog.terribledev.io/{a.Url}/" }).ToList()
|
||||
};
|
||||
sitemap.Urls.AddRange(sitewideLinks);
|
||||
ser.Serialize(this.Response.Body, sitemap);
|
||||
|
||||
@@ -3,31 +3,24 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Controllers
|
||||
{
|
||||
public class TagsController : Controller
|
||||
{
|
||||
private readonly PostCache postCache;
|
||||
|
||||
public TagsController(PostCache postCache)
|
||||
{
|
||||
this.postCache = postCache;
|
||||
}
|
||||
[Route("/all-tags")]
|
||||
[OutputCache(Duration = 31536000)]
|
||||
public IActionResult AllTags()
|
||||
{
|
||||
return View(postCache.TagsToPosts);
|
||||
return View(HomeController.tagToPost);
|
||||
}
|
||||
[Route("/tag/{tagName}")]
|
||||
[OutputCache(Duration = 31536000, VaryByParam = "tagName")]
|
||||
public IActionResult GetTag(string tagName)
|
||||
{
|
||||
if(!postCache.TagsToPosts.TryGetValue(tagName.ToLower(), out var models))
|
||||
if(!HomeController.tagToPost.TryGetValue(tagName, out var models))
|
||||
{
|
||||
return Redirect($"/404/?from=/tag/{tagName}/");
|
||||
return NotFound();
|
||||
}
|
||||
{
|
||||
return View(new Models.GetTagViewModel { Tag = tagName, Posts = models });
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public static SyndicationItem ToSyndicationItem(this IPost x)
|
||||
{
|
||||
Uri.TryCreate($"https://blog.terribledev.io/{x.Url}/", UriKind.Absolute, out var url);
|
||||
Uri.TryCreate($"https://blog.terribledev.io/{x.Url}", UriKind.Absolute, out var url);
|
||||
var syn = new SyndicationItem()
|
||||
{
|
||||
Title = x.Title,
|
||||
@@ -20,7 +20,6 @@ namespace TerribleDev.Blog.Web
|
||||
Published = x.PublishDate
|
||||
};
|
||||
syn.AddLink(new SyndicationLink(url));
|
||||
return syn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
using System.Linq;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Factories
|
||||
{
|
||||
public static class BlogCacheFactory
|
||||
{
|
||||
public static PostCache ProjectPostCache(IEnumerable<IPost> rawPosts)
|
||||
{
|
||||
var posts = rawPosts.OrderByDescending(a => a.PublishDate).ToImmutableList();
|
||||
var tagsToPost = posts.Where(a=>a.tags != null)
|
||||
.Aggregate(
|
||||
ImmutableDictionary.Create<string, ImmutableList<IPost>>(),
|
||||
(accum, item) => {
|
||||
foreach(var tag in item.tags.Select(i => i.ToLower()))
|
||||
{
|
||||
if(accum.TryGetValue(tag, out var list))
|
||||
{
|
||||
accum = accum.SetItem(tag, list.Add(item));
|
||||
}
|
||||
else
|
||||
{
|
||||
accum = accum.Add(tag, ImmutableList.Create<IPost>(item));
|
||||
}
|
||||
}
|
||||
return accum;
|
||||
}).ToImmutableSortedDictionary();
|
||||
var urlToPosts = posts.ToImmutableDictionary(a => a.Url);
|
||||
var postsByPage =
|
||||
posts.Aggregate(ImmutableDictionary.Create<int, ImmutableList<IPost>>(), (accum, item) =>
|
||||
{
|
||||
if(!accum.Keys.Any())
|
||||
{
|
||||
accum = accum.SetItem(1, ImmutableList.Create<IPost>());
|
||||
}
|
||||
var highestPage = accum.Keys.Any() ? accum.Keys.Max() : 1;
|
||||
var current = accum[highestPage];
|
||||
if (current.Count >= 10)
|
||||
{
|
||||
return accum.Add(highestPage + 1, ImmutableList.Create(item));
|
||||
}
|
||||
|
||||
return accum.SetItem(highestPage, current.Add(item));
|
||||
}).ToImmutableDictionary();
|
||||
var syndicationPosts = posts.Select(i => i.ToSyndicationItem()).ToImmutableList();
|
||||
return new PostCache()
|
||||
{
|
||||
PostsAsLists = posts,
|
||||
TagsToPosts = tagsToPost,
|
||||
UrlToPost = urlToPosts,
|
||||
PostsByPage = postsByPage,
|
||||
PostsAsSyndication = syndicationPosts
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,21 +9,17 @@ using Microsoft.AspNetCore.Html;
|
||||
using Markdig;
|
||||
using TerribleDev.Blog.Web.MarkExtension.TerribleDev.Blog.Web.ExternalLinkParser;
|
||||
using TerribleDev.Blog.Web.MarkExtension;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using System.Diagnostics;
|
||||
using TerribleDev.Blog.Web.Factories;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public class BlogFactory
|
||||
{
|
||||
private HighlightFactory highlightFactory = new HighlightFactory();
|
||||
public List<IPost> GetAllPosts(string domain)
|
||||
public List<IPost> GetAllPosts()
|
||||
{
|
||||
// why didn't I use f# I'd have a pipe operator by now
|
||||
var posts = GetPosts();
|
||||
var postsAsText = posts.Select(GetFileText);
|
||||
return Task.WhenAll(postsAsText).Result.Select(b => ParsePost(b.text, b.fileInfo.Name, domain)).ToList();
|
||||
return Task.WhenAll(postsAsText).Result.AsParallel().Select(b => ParsePost(b.text, b.fileInfo.Name)).ToList();
|
||||
}
|
||||
|
||||
private static async Task<(string text, FileInfo fileInfo)> GetFileText(string filePath)
|
||||
@@ -42,31 +38,26 @@ namespace TerribleDev.Blog.Web
|
||||
return serializer.Deserialize<PostSettings>(ymlText);
|
||||
|
||||
}
|
||||
public IPost ParsePost(string postText, string fileName, string domain)
|
||||
public IPost ParsePost(string postText, string fileName)
|
||||
{
|
||||
var splitFile = postText.Split("---");
|
||||
var ymlRaw = splitFile[0];
|
||||
var markdownText = string.Join("", splitFile.Skip(1));
|
||||
var postSettings = ParseYaml(ymlRaw);
|
||||
var resolvedUrl = !string.IsNullOrWhiteSpace(postSettings.permalink) ? postSettings.permalink : fileName.Split('.')[0].Replace(' ', '-').WithoutSpecialCharacters();
|
||||
var codeBlocks = new List<string>();
|
||||
List<string> postImages = new List<string>();
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.Use(new AbsoluteLinkConverter(resolvedUrl, domain))
|
||||
.Use<ImageRecorder>(new ImageRecorder(ref postImages))
|
||||
.Use<PictureInline>(new PictureInline(resolvedUrl))
|
||||
.Use<TargetLinkExtension>()
|
||||
.Use<PictureInline>()
|
||||
.Use(new CodeRecorder(ref codeBlocks))
|
||||
.Use<ImageRecorder>(new ImageRecorder(ref postImages))
|
||||
.UseMediaLinks()
|
||||
.UseEmojiAndSmiley()
|
||||
.Build();
|
||||
var postContent = Markdown.ToHtml(markdownText, pipeline);
|
||||
var postContentHighlighted = highlightFactory.Highlight(postContent);
|
||||
var postContentPlain = String.Join("", Markdown.ToPlainText(markdownText, pipeline).Split("<!-- more -->"));
|
||||
|
||||
var summary = postContent.Split("<!-- more -->")[0];
|
||||
var postSummaryPlain = postContentPlain.Split("<!-- more -->")[0];
|
||||
|
||||
return new Post()
|
||||
{
|
||||
PublishDate = postSettings.date.ToUniversalTime(),
|
||||
@@ -78,8 +69,7 @@ namespace TerribleDev.Blog.Web
|
||||
SummaryPlain = postSummaryPlain,
|
||||
SummaryPlainShort = (postContentPlain.Length <= 147 ? postContentPlain : postContentPlain.Substring(0, 146)) + "...",
|
||||
ContentPlain = postContentPlain,
|
||||
Images = postImages.Distinct().ToList(),
|
||||
CodeBlockLangs = codeBlocks
|
||||
Images = postImages.Distinct().Select(a => a.StartsWith('/') ? a : $"/{resolvedUrl}/{a}").ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Factories
|
||||
{
|
||||
public class HighlightFactory
|
||||
{
|
||||
private Regex codeFenceLang = new Regex("(?=<code class=\"language-(.*?)\">(.*?)(?=</code>))", RegexOptions.Compiled | RegexOptions.Singleline);
|
||||
public string Highlight(string input)
|
||||
{
|
||||
return codeFenceLang.Replace(input, m => {
|
||||
return m.ToString();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Factories.Processors
|
||||
{
|
||||
public class JavaScriptProcessor
|
||||
{
|
||||
private Regex keywordRegex = new Regex(@"\b(in|of|if|for|while|finally|var|new|function|do|return|void|else|break|catch|instanceof|with|throw|case|default|try|this|switch|continue|typeof|delete|let|yield|const|export|super|debugger|as|async|await|static|import|from|as|)(?=[^\w])", RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
private Regex literalRegex = new Regex(@"\b(true|false|null|undefined|NaN|Infinity)(?=[^\w])", RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
private Regex[] quoteMarkRegexes = new Regex[]{ new Regex("(.*?)", RegexOptions.Compiled | RegexOptions.Multiline), new Regex(@"'(.*?)'", RegexOptions.Compiled | RegexOptions.Multiline)};
|
||||
public string Process(string input)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
public class AbsoluteLinkConverter : IMarkdownExtension
|
||||
{
|
||||
public string BaseUrl { get; }
|
||||
public string Domain { get; }
|
||||
|
||||
public AbsoluteLinkConverter(string baseUrl, string domain)
|
||||
{
|
||||
BaseUrl = baseUrl;
|
||||
Domain = domain;
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
{
|
||||
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();
|
||||
inlineRenderer.TryWriters.Add(TryLinkAbsoluteUrlWriter);
|
||||
}
|
||||
}
|
||||
private bool TryLinkAbsoluteUrlWriter(HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
var prevDynamic = linkInline.GetDynamicUrl;
|
||||
linkInline.GetDynamicUrl = () => {
|
||||
var escapeUrl = prevDynamic != null ? prevDynamic() ?? linkInline.Url : linkInline.Url;
|
||||
if(!System.Uri.TryCreate(escapeUrl, UriKind.RelativeOrAbsolute, out var parsedResult))
|
||||
{
|
||||
throw new Exception($"Error making link for {escapeUrl} @ {BaseUrl}");
|
||||
}
|
||||
if(parsedResult.IsAbsoluteUri)
|
||||
{
|
||||
return escapeUrl;
|
||||
}
|
||||
var uriBuilder = new UriBuilder(Domain);
|
||||
if(!escapeUrl.StartsWith("/"))
|
||||
{
|
||||
uriBuilder = uriBuilder.WithPathSegment($"/{BaseUrl}/{escapeUrl}");
|
||||
}
|
||||
else
|
||||
{
|
||||
uriBuilder = uriBuilder.WithPathSegment(parsedResult.ToString());
|
||||
}
|
||||
return uriBuilder.Uri.ToString();
|
||||
};
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
public class CodeRecorder : IMarkdownExtension
|
||||
{
|
||||
public CodeRecorder(ref List<string> codeLanguages)
|
||||
{
|
||||
CodeLanguages = codeLanguages;
|
||||
}
|
||||
|
||||
public List<string> CodeLanguages { get; }
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
{
|
||||
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<CodeBlockRenderer>();
|
||||
if (inlineRenderer != null)
|
||||
{
|
||||
inlineRenderer.TryWriters.Add(TryWriter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryWriter(HtmlRenderer renderer, CodeBlock block)
|
||||
{
|
||||
var fencedBlock = block as FencedCodeBlock;
|
||||
if(fencedBlock == null || fencedBlock.Info == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
CodeLanguages.Add(fencedBlock.Info ?? "");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,6 @@ namespace TerribleDev.Blog.Web.MarkExtension
|
||||
|
||||
private void RenderTargetAttribute(Uri uri, HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
|
||||
linkInline.SetAttributes(new HtmlAttributes() { Properties = new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("target", "_blank"), new KeyValuePair<string, string>("rel", "noopener"), } });
|
||||
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace TerribleDev.Blog.Web.MarkExtension
|
||||
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();
|
||||
if (inlineRenderer != null)
|
||||
{
|
||||
inlineRenderer.TryWriters.Remove(TryLinkInlineRenderer);
|
||||
inlineRenderer.TryWriters.Add(TryLinkInlineRenderer);
|
||||
}
|
||||
}
|
||||
@@ -48,8 +49,7 @@ namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var url = linkInline.GetDynamicUrl != null ? linkInline.GetDynamicUrl(): linkInline.Url;
|
||||
this.images.Add(url);
|
||||
this.images.Add(linkInline.Url);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
34
src/TerribleDev.Blog.Web/MarkExtension/LinkConverter.cs
Normal file
34
src/TerribleDev.Blog.Web/MarkExtension/LinkConverter.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
|
||||
namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
public class LinkConverter : IMarkdownExtension
|
||||
{
|
||||
private readonly Func<string, string> convertLink;
|
||||
|
||||
public LinkConverter(Func<string, string> convertLink)
|
||||
{
|
||||
this.convertLink = convertLink;
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if(htmlRenderer == null) return;
|
||||
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();
|
||||
if(inlineRenderer == null) return;
|
||||
inlineRenderer.TryWriters.Add((ren, inline) => {
|
||||
return false;
|
||||
inline.GetDynamicUrl =
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,17 @@ using System;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
public class PictureInline : IMarkdownExtension
|
||||
{
|
||||
private readonly string postUrl;
|
||||
public PictureInline(string postUrl)
|
||||
{
|
||||
this.postUrl = postUrl;
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
@@ -15,54 +20,9 @@ namespace TerribleDev.Blog.Web.MarkExtension
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
if (htmlRenderer != null && !htmlRenderer.ObjectRenderers.Contains<PictureInlineRenderer>())
|
||||
{
|
||||
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();
|
||||
inlineRenderer.TryWriters.Add(TryLinkInlineRenderer);
|
||||
}
|
||||
}
|
||||
private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
if (linkInline == null || !linkInline.IsImage)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
renderer.Write("<picture>");
|
||||
WriteImageTag(renderer, linkInline, ".webp", "image/webp");
|
||||
WriteImageTag(renderer, linkInline, string.Empty);
|
||||
renderer.Write("</picture>");
|
||||
return true;
|
||||
|
||||
}
|
||||
private void WriteImageTag(HtmlRenderer renderer, LinkInline link, string suffix, string type = null)
|
||||
{
|
||||
|
||||
|
||||
renderer.Write(string.IsNullOrWhiteSpace(type) ? $"<img src=\"" : $"<source type=\"{type}\" srcset=\"");
|
||||
var escapeUrl = link.GetDynamicUrl != null ? link.GetDynamicUrl() ?? link.Url : link.Url;
|
||||
|
||||
renderer.WriteEscapeUrl($"{escapeUrl}{suffix}");
|
||||
renderer.Write("\"");
|
||||
renderer.WriteAttributes(link);
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write(" alt=\"");
|
||||
}
|
||||
var wasEnableHtmlForInline = renderer.EnableHtmlForInline;
|
||||
renderer.EnableHtmlForInline = false;
|
||||
renderer.WriteChildren(link);
|
||||
renderer.EnableHtmlForInline = wasEnableHtmlForInline;
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write("\"");
|
||||
}
|
||||
|
||||
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write(" />");
|
||||
htmlRenderer.ObjectRenderers.ReplaceOrAdd<LinkInlineRenderer>(new PictureInlineRenderer(postUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
// <summary>
|
||||
/// A HTML renderer for a <see cref="LinkInline"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{Markdig.Syntax.Inlines.LinkInline}" />
|
||||
public class PictureInlineRenderer : LinkInlineRenderer
|
||||
{
|
||||
private readonly string baseUrl;
|
||||
public PictureInlineRenderer(string baseUrl)
|
||||
{
|
||||
this.baseUrl = baseUrl;
|
||||
|
||||
}
|
||||
private void WriteImageTag(HtmlRenderer renderer, LinkInline link, string suffix, string type = null)
|
||||
{
|
||||
renderer.Write(string.IsNullOrWhiteSpace(type) ? $"<img src=\"" : $"<source type=\"{type}\" srcset=\"");
|
||||
var escapeUrl = link.GetDynamicUrl != null ? link.GetDynamicUrl() ?? link.Url : link.Url;
|
||||
//todo: this should be a seperate plugin
|
||||
// urls that are like "3.png" should resolve to /<postUrl>/3.png mostly for rss readers
|
||||
if(!System.Uri.TryCreate(escapeUrl, UriKind.RelativeOrAbsolute, out var parsedResult))
|
||||
{
|
||||
throw new Exception($"Error making link for {escapeUrl} @ {baseUrl}");
|
||||
}
|
||||
if(!escapeUrl.StartsWith("/"))
|
||||
{
|
||||
escapeUrl = $"{baseUrl}/{escapeUrl}";
|
||||
}
|
||||
renderer.WriteEscapeUrl($"{escapeUrl}{suffix}");
|
||||
renderer.Write("\"");
|
||||
renderer.WriteAttributes(link);
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write(" alt=\"");
|
||||
}
|
||||
var wasEnableHtmlForInline = renderer.EnableHtmlForInline;
|
||||
renderer.EnableHtmlForInline = false;
|
||||
renderer.WriteChildren(link);
|
||||
renderer.EnableHtmlForInline = wasEnableHtmlForInline;
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write("\"");
|
||||
}
|
||||
|
||||
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write(" />");
|
||||
}
|
||||
}
|
||||
protected override void Write(HtmlRenderer renderer, LinkInline link)
|
||||
{
|
||||
if (!link.IsImage)
|
||||
{
|
||||
base.Write(renderer, link);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
renderer.Write("<picture>");
|
||||
WriteImageTag(renderer, link, ".webp", "image/webp");
|
||||
WriteImageTag(renderer, link, string.Empty);
|
||||
renderer.Write("</picture>");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,5 @@ namespace TerribleDev.Blog.Web.Models
|
||||
string SummaryPlainShort { get; set; }
|
||||
IList<string> tags { get; set; }
|
||||
IList<string> Images { get; set;}
|
||||
IList<string> CodeBlockLangs { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,5 @@ namespace TerribleDev.Blog.Web.Models
|
||||
public string SummaryPlainShort { get; set; }
|
||||
public IList<string> tags { get; set; }
|
||||
public IList<string> Images { get; set;}
|
||||
public IList<string> CodeBlockLangs { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.SyndicationFeed;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class PostCache
|
||||
{
|
||||
public ImmutableList<IPost> PostsAsLists { get; set;}
|
||||
public ImmutableSortedDictionary<string, ImmutableList<IPost>> TagsToPosts { get; set; }
|
||||
public ImmutableDictionary<string, IPost> UrlToPost { get; set; }
|
||||
public ImmutableDictionary<int, ImmutableList<IPost>> PostsByPage { get; set; }
|
||||
public ImmutableList<SyndicationItem> PostsAsSyndication { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ Getting Started:
|
||||
|
||||
Ok, so the alexa .net sdk is for the full framework only, and its built for webapi. The best way to get going is in visual studio `file -> new project -> ASP.NET Web Application .net framework` A dialog comes up, and I picked `Azure API App`.
|
||||
|
||||

|
||||

|
||||
|
||||
Now you have an empty webapi project. We don't need swashbuckle/swagger so lets get rid of that
|
||||
|
||||
|
||||
@@ -26,46 +26,6 @@ So what I personally like to do is find orange bars that often make up the bulk
|
||||
|
||||
So digging into other blog posts, I found posts showing how to [visualize your redux actions](https://medium.com/@vcarl/performance-profiling-a-redux-app-c85e67bf84ae) using the same performance API mechanisms react uses. That blog post uses redux middleware to add timings to actions. This narrowed down on our performance problems, but did not point out the exact selector that was slow. Clearly we had an action that was triggering an expensive state update, but the time was still spent in `anonymous function`. Thats when I had the idea to wrap reselect selector functions in a function that can append the timings. [This gist is what I came up with](https://gist.github.com/TerribleDev/db48b2c8e143f9364292161346877f93)
|
||||
|
||||
```js
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
const hasPerformanceApi =
|
||||
window &&
|
||||
window.performance &&
|
||||
window.performance.measure &&
|
||||
window.performance.mark;
|
||||
|
||||
const createFuncWithMark = (name, callback) => (...args) => {
|
||||
const startMark = `${name}-Startmark`;
|
||||
const endMark = `${name}-EndMark`;
|
||||
window.performance.mark(startMark);
|
||||
const result = callback(...args);
|
||||
window.performance.mark(endMark);
|
||||
window.performance.measure('♻️ ' + `${name}-Selector`, startMark, endMark);
|
||||
window.performance.clearMarks(startMark);
|
||||
window.performance.clearMarks(endMark);
|
||||
window.performance.clearMeasures(startMark);
|
||||
window.performance.clearMeasures(endMark);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const createMarkedSelector = (name, ...args) => {
|
||||
if (!hasPerformanceApi) {
|
||||
return createSelector(...args);
|
||||
}
|
||||
if (!name || typeof name !== 'string') {
|
||||
throw new Error('marked selectors must have names');
|
||||
}
|
||||
const callback = args.pop();
|
||||
const funcWithMark = createFuncWithMark(name, callback);
|
||||
args.push(funcWithMark);
|
||||
return createSelector(...args);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
So how does this work exactly? Well its a library that wraps the function you pass to reselect that adds markers to the window to tell you how fast reselect selectors take to run. Combined with the previously mentioned blog post, you can now get timings in chrome's performance tool with selectors! You can also combine this with the [redux middleware](https://medium.com/@vcarl/performance-profiling-a-redux-app-c85e67bf84ae) I previously mentioned to get a deeper insight into how your app is performing
|
||||
|
||||

|
||||
|
||||
@@ -16,7 +16,6 @@ using Microsoft.Net.Http.Headers;
|
||||
using HardHat.Middlewares;
|
||||
using HardHat;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
using TerribleDev.Blog.Web.Factories;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
@@ -43,10 +42,6 @@ namespace TerribleDev.Blog.Web
|
||||
{
|
||||
services.AddSingleton(getBlog());
|
||||
}
|
||||
services.AddSingleton(i => {
|
||||
var posts = new BlogFactory().GetAllPosts(Env.IsDevelopment() ? "https://localhost:5001": "https://blog.terribledev.io");
|
||||
return BlogCacheFactory.ProjectPostCache(posts);
|
||||
});
|
||||
services.AddResponseCompression(a =>
|
||||
{
|
||||
a.EnableForHttps = true;
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.0.2105168" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.0" />
|
||||
<PackageReference Include="UriBuilder.Fluent" Version="1.5.2" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.3.0" />
|
||||
<PackageReference Include="HardHat" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
|
||||
@@ -20,24 +20,14 @@
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="@Model.Title">
|
||||
<meta name="twitter:description" content="@Model.SummaryPlainShort">
|
||||
<meta name="twitter:site" content="@@TerribleDev">
|
||||
<meta name="twitter:creator" content="@@TerribleDev">
|
||||
<meta property="og:image" content="https://www.gravatar.com/avatar/333e3cea32cd17ff2007d131df336061?s=640" />
|
||||
@foreach(var image in Model.Images.Take(6))
|
||||
{
|
||||
<meta property="og:image" content="@image">
|
||||
<meta property="og:image" content="https://blog.terribledev.io@(image)">
|
||||
}
|
||||
@if(Model.Images.Count > 0)
|
||||
{
|
||||
<meta name="twitter:image" content="@(Model.Images[0])">
|
||||
}
|
||||
<meta property="og:image" content="https://www.gravatar.com/avatar/333e3cea32cd17ff2007d131df336061?s=640" />
|
||||
@if(Model.CodeBlockLangs.Count > 0)
|
||||
{
|
||||
<meta name="twitter:image" content="https://blog.terribledev.io@(Model.Images[0])">
|
||||
}
|
||||
}
|
||||
@section Scripts
|
||||
{
|
||||
@if(Model.CodeBlockLangs.Count > 0)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
@using Newtonsoft.Json
|
||||
@model IPost
|
||||
@model IPost
|
||||
|
||||
<article itemprop="blogPost">
|
||||
<h1 itemprop="headline" class="headline">@Model.Title</h1>
|
||||
@@ -16,12 +15,4 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<environment names="Development">
|
||||
<pre>
|
||||
<code>
|
||||
@JsonConvert.SerializeObject(Model, Formatting.Indented)
|
||||
</code>
|
||||
</pre>
|
||||
</environment>
|
||||
|
||||
</article>
|
||||
@@ -6,8 +6,6 @@
|
||||
<link rel="preconnect" href="https://www.google-analytics.com">
|
||||
<link rel="preconnect" href="https://stats.g.doubleclick.net">
|
||||
<link rel="preconnect" href="https://www.googletagmanager.com">
|
||||
<link rel="preconnect" href="https://az416426.vo.msecnd.net" />
|
||||
<link rel="preconnect" href="https://dc.services.visualstudio.com" />
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-48128396-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
@@ -36,12 +36,12 @@
|
||||
@RenderBody()
|
||||
</div>
|
||||
</main>
|
||||
@RenderSection("Scripts", required: false)
|
||||
<environment names="Development">
|
||||
<script asp-append-version="true" src="~/js/swi.js" async></script>
|
||||
</environment>
|
||||
<environment names="Production">
|
||||
<script asp-append-version="true" src="~/js/site.min.js" async></script>
|
||||
</environment>
|
||||
@RenderSection("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@using System.Collections.Immutable;
|
||||
@model IDictionary<string, ImmutableList<IPost>>
|
||||
@model Dictionary<string, List<IPost>>
|
||||
@{
|
||||
ViewData["Title"] = "all-tags";
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"ApplicationInsights": {
|
||||
"InstrumentationKey": "974b47d2-1f08-42df-b498-bbfda7425f0b"
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,8 @@
|
||||
"Blog": {
|
||||
"title": "The Ramblings of TerribleDev",
|
||||
"link": "https://blog.terribledev.io"
|
||||
},
|
||||
"ApplicationInsights": {
|
||||
"InstrumentationKey": "974b47d2-1f08-42df-b498-bbfda7425f0b"
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
.hljs,
|
||||
.hljs-subst {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.hljs-comment {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-attribute,
|
||||
.hljs-selector-tag,
|
||||
.hljs-meta-keyword,
|
||||
.hljs-doctag,
|
||||
.hljs-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
/* User color: hue: 0 */
|
||||
|
||||
.hljs-type,
|
||||
.hljs-string,
|
||||
.hljs-number,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-quote,
|
||||
.hljs-template-tag,
|
||||
.hljs-deletion {
|
||||
color: #880000;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #880000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-regexp,
|
||||
.hljs-symbol,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-link,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #BC6060;
|
||||
}
|
||||
|
||||
|
||||
/* Language color: hue: 90; */
|
||||
|
||||
.hljs-literal {
|
||||
color: #78A960;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-bullet,
|
||||
.hljs-code,
|
||||
.hljs-addition {
|
||||
color: #397300;
|
||||
}
|
||||
|
||||
|
||||
/* Meta color: hue: 200 */
|
||||
|
||||
.hljs-meta {
|
||||
color: #1f7199;
|
||||
}
|
||||
|
||||
.hljs-meta-string {
|
||||
color: #4d99bf;
|
||||
}
|
||||
|
||||
|
||||
/* Misc effects */
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Reference in New Issue
Block a user