This commit is contained in:
Tommy Parnell
2022-03-08 13:48:37 -05:00
parent 7e161d8d33
commit aae2a1d9e5
21 changed files with 288 additions and 57 deletions

View File

@@ -65,12 +65,17 @@ namespace TerribleDev.Blog.Web.Controllers
{
return Redirect($"/404/?from=/{postUrl}/{amp}/");
}
if(!postCache.UrlToPost.TryGetValue(postUrl, out var currentPost))
if(postCache.UrlToPost.TryGetValue(postUrl, out var currentPost))
{
this.StatusCode(404);
return View(nameof(FourOhFour));
return View("Post", model: new PostViewModel() { Post = currentPost, IsAmp = amp == "amp" });
}
return View("Post", model: new PostViewModel() { Post = currentPost, IsAmp = amp == "amp" });
if(postCache.LandingPagesUrl.TryGetValue(postUrl, out var landingPage))
{
return View("Post", model: new PostViewModel() { Post = landingPage, IsAmp = amp == "amp" });
}
this.StatusCode(404);
return View(nameof(FourOhFour));
}
[Route("/Error")]

View File

@@ -9,7 +9,7 @@ namespace TerribleDev.Blog.Web
{
public static class IPostExtensions
{
public static SyndicationItem ToSyndicationItem(this IPost x)
public static SyndicationItem ToSyndicationItem(this Post x)
{
Uri.TryCreate(x.CanonicalUrl, UriKind.Absolute, out var url);
var syn = new SyndicationItem()
@@ -22,7 +22,7 @@ namespace TerribleDev.Blog.Web
syn.AddLink(new SyndicationLink(url));
return syn;
}
public static ISet<string> ToNormalizedTagList(this IPost x)
public static ISet<string> ToNormalizedTagList(this Post x)
{
if(x.tags == null)
{

View File

@@ -11,73 +11,84 @@ namespace TerribleDev.Blog.Web.Factories
{
public static class BlogCacheFactory
{
public static PostCache ProjectPostCache(IEnumerable<IPost> rawPosts)
{
var orderedPosts = rawPosts.OrderByDescending(a => a.PublishDate);
var posts = new List<IPost>(orderedPosts);
var urlToPosts = new Dictionary<string, IPost>();
var tagsToPost = new Dictionary<string, IList<IPost>>();
var postsByPage = new Dictionary<int, IList<IPost>>();
var tagsToPost = new Dictionary<string, IList<Post>>();
var postsByPage = new Dictionary<int, IList<Post>>();
var syndicationPosts = new List<SyndicationItem>();
var landingPagesUrl = new Dictionary<string, LandingPage>();
var blogPostsLD = new List<Schema.NET.IBlogPosting>();
foreach(var post in orderedPosts)
foreach (var post in orderedPosts)
{
urlToPosts.Add(post.UrlWithoutPath, post);
syndicationPosts.Add(post.ToSyndicationItem());
blogPostsLD.Add(post.Content.Value.JsonLD);
foreach(var tag in post.ToNormalizedTagList())
if (post is Post)
{
if(tagsToPost.TryGetValue(tag, out var list))
var castedPost = post as Post;
urlToPosts.Add(post.UrlWithoutPath, castedPost);
syndicationPosts.Add(castedPost.ToSyndicationItem());
blogPostsLD.Add(post.Content.Value.JsonLD);
foreach (var tag in castedPost.ToNormalizedTagList())
{
list.Add(post);
if (tagsToPost.TryGetValue(tag, out var list))
{
list.Add(castedPost);
}
else
{
tagsToPost.Add(tag, new List<Post>() { castedPost });
}
}
if (postsByPage.Keys.Count < 1)
{
postsByPage.Add(1, new List<Post>() { castedPost });
}
else
{
tagsToPost.Add(tag, new List<IPost>() { post });
var highestPageKey = postsByPage.Keys.Max();
var highestPage = postsByPage[highestPageKey];
if (highestPage.Count < 10)
{
highestPage.Add(castedPost);
}
else
{
postsByPage.Add(highestPageKey + 1, new List<Post>() { castedPost });
}
}
}
if(postsByPage.Keys.Count < 1)
if (post is LandingPage)
{
postsByPage.Add(1, new List<IPost>() { post });
}
else
{
var highestPageKey = postsByPage.Keys.Max();
var highestPage = postsByPage[highestPageKey];
if(highestPage.Count < 10)
{
highestPage.Add(post);
}
else
{
postsByPage.Add(highestPageKey + 1, new List<IPost>() { post });
}
var castedPost = post as LandingPage;
landingPagesUrl.Add(castedPost.UrlWithoutPath, castedPost);
}
}
var ld = new Schema.NET.Blog()
{
Name = "TerribleDev Blog",
Description = "The blog of Tommy Parnell",
Author = new Schema.NET.Person() { Name = "TerribleDev" },
Image = new Schema.NET.ImageObject() { Url = new Schema.NET.OneOrMany<Uri>(new Uri("https://blog.terrible.dev/content/tommyAvatar4.jpg")) },
Url = new Schema.NET.OneOrMany<Uri>(new Uri("https://blog.terrible.dev/" )),
SameAs = new Schema.NET.OneOrMany<Uri>(new Uri("https://twitter.com/terribledev")),
BlogPost = new Schema.NET.OneOrMany<Schema.NET.IBlogPosting>(blogPostsLD),
};
var ld = new Schema.NET.Blog()
{
Name = "TerribleDev Blog",
Description = "The blog of Tommy Parnell",
Author = new Schema.NET.Person() { Name = "TerribleDev" },
Image = new Schema.NET.ImageObject() { Url = new Schema.NET.OneOrMany<Uri>(new Uri("https://blog.terrible.dev/content/tommyAvatar4.jpg")) },
Url = new Schema.NET.OneOrMany<Uri>(new Uri("https://blog.terrible.dev/")),
SameAs = new Schema.NET.OneOrMany<Uri>(new Uri("https://twitter.com/terribledev")),
BlogPost = new Schema.NET.OneOrMany<Schema.NET.IBlogPosting>(blogPostsLD),
};
var website = new Schema.NET.WebSite()
{
Name = "TerribleDev Blog",
Description = "The blog of Tommy Parnell",
Author = new Schema.NET.Person() { Name = "TerribleDev" },
Image = new Schema.NET.ImageObject() { Url = new Schema.NET.OneOrMany<Uri>(new Uri("https://blog.terrible.dev/content/tommyAvatar4.jpg")) },
Url = new Schema.NET.OneOrMany<Uri>(new Uri("https://blog.terrible.dev/" )),
Url = new Schema.NET.OneOrMany<Uri>(new Uri("https://blog.terrible.dev/")),
SameAs = new Schema.NET.OneOrMany<Uri>(new Uri("https://twitter.com/terribledev")),
PotentialAction = new Schema.NET.OneOrMany<Schema.NET.IAction>(
// search action
new List<Schema.NET.SearchAction>()
new List<Schema.NET.SearchAction>()
{
new Schema.NET.SearchAction()
{
@@ -95,13 +106,14 @@ namespace TerribleDev.Blog.Web.Factories
ValueMaxLength = 500,
}
)
)
)
}
}
)
};
return new PostCache()
{
LandingPagesUrl = landingPagesUrl,
PostsAsLists = posts,
TagsToPosts = tagsToPost,
UrlToPost = urlToPosts,

View File

@@ -71,6 +71,11 @@ namespace TerribleDev.Blog.Web
var resolvedUrl = !string.IsNullOrWhiteSpace(postSettings.permalink) ? postSettings.permalink : fileName.Split('.')[0].Replace(' ', '-').WithoutSpecialCharacters();
var canonicalUrl = $"https://blog.terrible.dev/{resolvedUrl}/";
var ampUrl = $"https://blog.terrible.dev/{resolvedUrl}/amp/";
return postSettings.isLanding ? BuildLandingPage(fileName, domain, markdownText, postSettings, resolvedUrl, canonicalUrl, ampUrl) : BuildPost(fileName, domain, markdownText, postSettings, resolvedUrl, canonicalUrl, ampUrl);
}
private Post BuildPost(string fileName, string domain, string markdownText, PostSettings postSettings, string resolvedUrl, string canonicalUrl, string ampUrl)
{
return new Post()
{
PublishDate = postSettings.date.ToUniversalTime(),
@@ -81,6 +86,7 @@ namespace TerribleDev.Blog.Web
CanonicalUrl = canonicalUrl,
AMPUrl = ampUrl,
UrlWithoutPath = resolvedUrl,
isLanding = postSettings.isLanding,
Content = new Lazy<IPostContent>(() =>
{
(string postContent, string postContentPlain, string summary, string postSummaryPlain, IList<string> postImages) = ResolveContentForPost(markdownText, fileName, resolvedUrl, domain);
@@ -129,5 +135,53 @@ namespace TerribleDev.Blog.Web
}),
};
}
private LandingPage BuildLandingPage(string fileName, string domain, string markdownText, PostSettings postSettings, string resolvedUrl, string canonicalUrl, string ampUrl)
{
return new LandingPage()
{
PublishDate = postSettings.date.ToUniversalTime(),
UpdatedDate = postSettings.updated?.ToUniversalTime() ?? null,
Title = postSettings.title,
RelativeUrl = $"/{resolvedUrl}/",
CanonicalUrl = canonicalUrl,
AMPUrl = ampUrl,
UrlWithoutPath = resolvedUrl,
isLanding = postSettings.isLanding,
Content = new Lazy<IPostContent>(() =>
{
(string postContent, string postContentPlain, string summary, string postSummaryPlain, IList<string> postImages) = ResolveContentForPost(markdownText, fileName, resolvedUrl, domain);
var breadcrumb = new Schema.NET.BreadcrumbList()
{
ItemListElement = new List<IListItem>() // Required
{
new ListItem() // Required
{
Position = 1, // Required
Url = new Uri("https://blog.terrible.dev/") // Required
},
new ListItem()
{
Position = 2,
Name = postSettings.title,
},
},
};
// regex remove picture and source tags but not the child elements
var postContentClean = Regex.Replace(postContent, "<picture.*?>|</picture>|<source.*?>|</source>", "", RegexOptions.Singleline);
return new PostContent()
{
AmpContent = new HtmlString(postContentClean),
Content = new HtmlString(postContent),
Images = postImages,
ContentPlain = postContentPlain,
Summary = new HtmlString(summary),
SummaryPlain = postSummaryPlain,
SummaryPlainShort = (postContentPlain.Length <= 147 ? postContentPlain : postContentPlain.Substring(0, 146)) + "...",
JsonLDBreadcrumb = breadcrumb,
JsonLDBreadcrumbString = breadcrumb.ToHtmlEscapedString().Replace("https://schema.org", "https://schema.org/true"),
};
}),
};
}
}
}

View File

@@ -16,8 +16,8 @@ namespace TerribleDev.Blog.Web.Models
string Title { get; set; }
DateTime PublishDate { get; set; }
DateTime? UpdatedDate { get; set; }
IList<string> tags { get; set; }
Lazy<IPostContent> Content { get; set; }
bool isLanding { get; set; }
}
}

View File

@@ -12,5 +12,6 @@ namespace TerribleDev.Blog.Web.Models
string thumbnailImage { get; set; }
DateTimeOffset date { get; set; }
DateTimeOffset updated { get; set; }
bool isLanding { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Html;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace TerribleDev.Blog.Web.Models
{
[DebuggerDisplay("{Title}")]
public class LandingPage : IPost
{
public string AMPUrl { get; set; }
public string CanonicalUrl { get; set; }
public string UrlWithoutPath { get; set; }
public string RelativeUrl { get; set; }
public string Title { get; set; }
public DateTime PublishDate { get; set; }
public DateTime? UpdatedDate { get; set; }
public Lazy<IPostContent> Content { get; set; }
public bool isLanding { get; set; } = false;
}
}

View File

@@ -18,5 +18,7 @@ namespace TerribleDev.Blog.Web.Models
public DateTime? UpdatedDate { get; set; }
public IList<string> tags { get; set; }
public Lazy<IPostContent> Content { get; set; }
public bool isLanding { get; set; } = false;
}
}

View File

@@ -6,15 +6,17 @@ namespace TerribleDev.Blog.Web.Models
public class PostCache
{
public IList<IPost> PostsAsLists { get; set;}
public IDictionary<string, IList<IPost>> TagsToPosts { get; set; }
public IDictionary<string, IList<Post>> TagsToPosts { get; set; }
public IDictionary<string, IPost> UrlToPost { get; set; }
public IDictionary<int, IList<IPost>> PostsByPage { get; set; }
public IDictionary<int, IList<Post>> PostsByPage { get; set; }
public IList<SyndicationItem> PostsAsSyndication { get; set; }
public Schema.NET.Blog BlogLD { get; set; }
public Schema.NET.WebSite SiteLD { get; set; }
public string BlogLDString { get; set; }
public string SiteLDString { get; set; }
public Dictionary<string, LandingPage> LandingPagesUrl { get; set; }
}
}

View File

@@ -15,5 +15,7 @@ namespace TerribleDev.Blog.Web.Models
public string thumbnailImage { get; set; }
public string thumbnail_image_position { get; set; }
public string layout { get; set; }
public bool isLanding { get; set; } = false;
}
}

View File

@@ -80,7 +80,7 @@ So the major feature I was blown away by with NDepend was how clean, and organiz
The code quality rules, uses the NDepends querying engine to get your code. When you click on a rule the Linq query used will be displayed in a separate window. You can use this window to create your own rules, using the same querying engine. The following is a query to find code that should not be declared public.
<pre>
//<Name>Avoid public methods not publicly visible</Name>
//Avoid public methods not publicly visible
// Matched methods are declared public but are not publicly visible by assemblies consumers.
// Their visibility level must be decreased.

View File

@@ -339,4 +339,105 @@ Environment.Exit(result);
Here is the full source as a [gist](https://gist.github.com/TerribleDev/06abb67350745a58f9fab080bee74be1#file-program-cs):
<script src="https://gist.github.com/TerribleDev/06abb67350745a58f9fab080bee74be1.js"></script>
```csharp
public static void Main(string[] args)
{
var app = new Microsoft.Extensions.CommandLineUtils.CommandLineApplication();
var catapult = app.Command("catapult", config => {
config.OnExecute(()=>{
config.ShowHelp(); //show help for catapult
return 1; //return error since we didn't do anything
});
config.HelpOption("-? | -h | --help"); //show help on --help
});
catapult.Command("help", config => {
config.Description = "get help!";
config.OnExecute(()=>{
catapult.ShowHelp("catapult");
return 1;
});
});
catapult.Command("list", config => {
config.Description = "list catapults";
config.HelpOption("-? | -h | --help");
config.OnExecute(()=>{
Console.WriteLine("a");
Console.WriteLine("b");
return 0;
});
});
catapult.Command("add", config => {
config.Description = "Add a catapult";
config.HelpOption("-? | -h | --help");
var arg = config.Argument("name", "name of the catapult", false);
config.OnExecute(()=>{
if(!string.IsNullOrWhiteSpace(arg.Value))
{
//add snowballs somehow
Console.WriteLine($"added {arg.Value}");
return 0;
}
return 1;
});
});
catapult.Command("fling", config =>{
config.Description = "fling snow";
config.HelpOption("-? | -h | --help");
var ball = config.Argument("snowballId", "snowball id", false);
var cata = config.Argument("catapultId", "id of catapult to use", false);
config.OnExecute(()=>{
//actually do something
Console.WriteLine($"threw snowball: {ball.Value} with {cata.Value}");
return 0;
});
});
var snowball = app.Command("snowball", config => {
config.OnExecute(()=>{
config.ShowHelp(); //show help for catapult
return 1; //return error since we didn't do anything
});
config.HelpOption("-? | -h | --help"); //show help on --help
});
snowball.Command("help", config => {
config.Description = "get help!";
config.OnExecute(()=>{
catapult.ShowHelp("snowball");
return 1;
});
});
snowball.Command("list", config => {
config.HelpOption("-? | -h | --help");
config.Description = "list snowballs";
config.OnExecute(()=>{
Console.WriteLine("1");
Console.WriteLine("2");
return 0;
});
});
snowball.Command("add", config => {
config.Description = "Add a snowball";
config.HelpOption("-? | -h | --help");
var arg = config.Argument("name", "name of the snowball", false);
config.OnExecute(()=>{
if(!string.IsNullOrWhiteSpace(arg.Value))
{
//add snowballs somehow
Console.WriteLine($"added {arg.Value}");
return 0;
}
return 0;
});
});
//give people help with --help
app.HelpOption("-? | -h | --help");
var result = app.Execute(args);
Environment.Exit(result);
}
```

View File

@@ -34,5 +34,3 @@ Eventually we bit the bullet and decided to sign our requests to the cluster. Un
This project totally saved my bacon. Brandon's library plugged right into the .NET sdk, and auth'd our requests to aws without us having to figure out all that crypo. Within moments of finding it I filed an [issue](https://github.com/bcuff/elasticsearch-net-aws/issues/1) thanking Brandon as it really helped me out.
The Elasticsearch service offering by Amazon is pretty awesome. Like any platform its less flexible then hosting the instances yourself. You have to live with the plugins they ship, but on the plus side you get a full cluster, with monitoring, and a knob to turn up instances, or storage space without having to worry about the details.
<script src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

View File

@@ -0,0 +1,10 @@
title: About
date: 2022-03-08 01:03
isLanding: true
permalink: about
---
I am a software engineer. I currently work at [Quala](https://www.quala.io). I have worked on all area's of the stack. From a sysadmin, network engineer, backend developer, and frontend developer. I've helped build some extremely large scale websites such as [Vistaprint](https://www.vistaprint.com) and [CarGurus](https://www.cargurus.com). I have a passion for high performing software, devops, and front end. I am a huge fan of [JavaScript](https://en.wikipedia.org/wiki/JavaScript), [C#](https://en.wikipedia.org/wiki/C_Sharp), [Golang](https://en.wikipedia.org/wiki/Go_(programming_language)), and [Rust](https://en.wikipedia.org/wiki/Rust_(programming_language)).
I blog about my general pains building software.

View File

@@ -6,7 +6,7 @@
}
<cache vary-by-route="postUrl,amp">
@Html.DisplayFor(m => m.Post, "Post")
@await Html.PartialAsync("SharedPost", Model.Post)
</cache>
@section Head {

View File

@@ -0,0 +1,17 @@
@model LandingPage
@{
var amp = ViewData["amp"] as bool? ?? false;
}
<article>
<h1 itemprop="headline" class="headline">@Model.Title</h1>
<time class="headlineSubtext" itemprop="datePublished" content="@Model.PublishDate.ToString()">@Model.PublishDate.ToString("D")</time>
@if(amp)
{
@Model.Content.Value.AmpContent
}
else
{
@Model.Content.Value.Content
}
</article>

View File

@@ -1,4 +1,4 @@
@model IPost
@model Post
@{
var amp = ViewData["amp"] as bool? ?? false;
}

View File

@@ -18,6 +18,7 @@
<span>Tommy "Terrible Dev" Parnell</span>
<ul class="sidebarBtns">
<li><a href="/" class="link-unstyled">Home</a></li>
<li><a href="/about" class="link-unstyled">About</a></li>
<li><a href="/all-tags/" class="link-unstyled">Tags</a></li>
<li><a href="/rss.xml" class="link-unstyled">RSS Feed</a></li>
<li><a href="https://github.com/terribledev" rel="noopener" target="_blank" class="link-unstyled">Github</a></li>

View File

@@ -0,0 +1,3 @@
@model IPost
@Html.DisplayForModel()

View File

@@ -1,4 +1,4 @@
@model IDictionary<string, IList<IPost>>
@model IDictionary<string, IList<Post>>
@{
ViewData["Title"] = "all-tags";
}