Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8a0004bc5 | ||
|
|
b7486dcc85 | ||
|
|
ab9c9fdc90 | ||
|
|
60fe3d8c2a | ||
|
|
115580f0f4 | ||
|
|
c9229018db | ||
|
|
03eeef20d7 | ||
|
|
6ebf9a6574 | ||
|
|
7cf143c078 | ||
|
|
f7984258a5 | ||
|
|
712e92ff6b | ||
|
|
8bf5a55dcb | ||
|
|
04d5f29fee | ||
|
|
0aa95d9988 | ||
|
|
506188041a | ||
|
|
099f570e84 | ||
|
|
6813370179 | ||
|
|
f981ba3f39 | ||
|
|
365f1730f5 | ||
|
|
d1c2d60c5a | ||
|
|
7f28de0655 | ||
|
|
fb14bb735e | ||
|
|
fdbc9c6d6a | ||
|
|
a2e6c43e56 | ||
|
|
4acdfb3d4c | ||
|
|
45d0f3361e | ||
|
|
9044e8679f | ||
|
|
7852083a8c | ||
|
|
c353269c52 | ||
|
|
091abd4561 | ||
|
|
a2dd0c0d2a | ||
|
|
3143f1c76b | ||
|
|
fd5c668820 | ||
|
|
031c5c2598 | ||
|
|
a46631b5e6 | ||
|
|
4c752803c0 |
34
TerribleDev.Blog.MarkdownPlugins/Extensions.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace TerribleDev.Blog.MarkdownPlugins
|
||||
{
|
||||
public static class StringExtension
|
||||
{
|
||||
public static string WithoutSpecialCharacters(this string str)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (char c in str)
|
||||
{
|
||||
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '-')
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
public static string RemoveSpecialCharacters(string str)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (char c in str)
|
||||
{
|
||||
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '-')
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
62
TerribleDev.Blog.MarkdownPlugins/ExternalLinkParser.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace TerribleDev.Blog.MarkdownPlugins
|
||||
{
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
/// <summary>
|
||||
/// Extension for extending image Markdown links in case a video or an audio file is linked and output proper link.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.IMarkdownExtension" />
|
||||
public class TargetLinkExtension : IMarkdownExtension
|
||||
{
|
||||
|
||||
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>();
|
||||
if (inlineRenderer != null)
|
||||
{
|
||||
inlineRenderer.TryWriters.Remove(TryLinkInlineRenderer);
|
||||
inlineRenderer.TryWriters.Add(TryLinkInlineRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
if (linkInline.Url == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
// Only process absolute Uri
|
||||
if (!Uri.TryCreate(linkInline.Url, UriKind.RelativeOrAbsolute, out uri) || !uri.IsAbsoluteUri)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
RenderTargetAttribute(uri, renderer, linkInline);
|
||||
return false;
|
||||
}
|
||||
|
||||
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"), } });
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
48
TerribleDev.Blog.MarkdownPlugins/ImageRecorder.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
namespace TerribleDev.Blog.MarkdownPlugins
|
||||
{
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
public class ImageRecorder : IMarkdownExtension
|
||||
{
|
||||
private List<string> images;
|
||||
public ImageRecorder(List<string> images)
|
||||
{
|
||||
this.images = images;
|
||||
}
|
||||
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>();
|
||||
if (inlineRenderer != null)
|
||||
{
|
||||
inlineRenderer.TryWriters.Remove(TryLinkInlineRenderer);
|
||||
inlineRenderer.TryWriters.Add(TryLinkInlineRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
if (linkInline.Url == null || !linkInline.IsImage)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
this.images.Add(linkInline.Url);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Markdig" Version="0.15.7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -7,6 +7,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E6C01762-AEB
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TerribleDev.Blog.Web", "src\TerribleDev.Blog.Web\TerribleDev.Blog.Web.csproj", "{BAA8662D-6D38-4811-A6FF-7A61D0C633D2}"
|
||||
EndProject
|
||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "TerribleDev.Blog.Core", "src\TerribleDev.Blog.Core\TerribleDev.Blog.Core.fsproj", "{31876934-45BC-4C28-BBAF-0A50047345BB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerribleDev.Blog.MarkdownPlugins", "TerribleDev.Blog.MarkdownPlugins\TerribleDev.Blog.MarkdownPlugins.csproj", "{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -29,12 +33,38 @@ Global
|
||||
{BAA8662D-6D38-4811-A6FF-7A61D0C633D2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{BAA8662D-6D38-4811-A6FF-7A61D0C633D2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BAA8662D-6D38-4811-A6FF-7A61D0C633D2}.Release|x86.Build.0 = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{BAA8662D-6D38-4811-A6FF-7A61D0C633D2} = {E6C01762-AEBF-47C4-8D95-383504D8BC70}
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB} = {E6C01762-AEBF-47C4-8D95-383504D8BC70}
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7} = {E6C01762-AEBF-47C4-8D95-383504D8BC70}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {CFA796F1-4389-452F-B224-E64C72E907C4}
|
||||
|
||||
85
src/TerribleDev.Blog.Core/Factories/BlogFactory.fs
Normal file
@@ -0,0 +1,85 @@
|
||||
namespace TerribleDev.Blog.Core.Factories
|
||||
module BlogFactory =
|
||||
|
||||
open System.IO
|
||||
open YamlDotNet.Serialization
|
||||
open TerribleDev.Blog.Core.Models
|
||||
open TerribleDev.Blog.Core
|
||||
open Markdig
|
||||
open TerribleDev.Blog.MarkdownPlugins
|
||||
open Microsoft.AspNetCore.Html
|
||||
open System.Linq
|
||||
open System.Collections.Generic
|
||||
open fs
|
||||
open System
|
||||
|
||||
let fixTagName (tag:string) = tag.Replace(' ', '-').WithoutSpecialCharacters().ToLower()
|
||||
let mapImgUrlResolver (resolveUrl:string) =
|
||||
fun (imgUrl: string) -> if imgUrl.StartsWith('/') then imgUrl else sprintf "/%s/%s" resolveUrl imgUrl
|
||||
|
||||
|
||||
let getPosts (path) = Directory.EnumerateFiles(path, "*.md", SearchOption.TopDirectoryOnly)
|
||||
let parseYml (postText:string) =
|
||||
let split = postText.Split("---")
|
||||
match split with
|
||||
| [| _ |] -> raise(Exception("No yml found"))
|
||||
| [| yml; _|] -> DeserializerBuilder().Build().Deserialize(yml)
|
||||
| split when split.Length > 2 -> DeserializerBuilder().Build().Deserialize(split.[0])
|
||||
|
||||
|
||||
|
||||
let getMarkdownBuilder (imgRef) =
|
||||
MarkdownPipelineBuilder()
|
||||
.Use<TargetLinkExtension>()
|
||||
.Use<ImageRecorder>(new ImageRecorder(imgRef))
|
||||
.UseMediaLinks()
|
||||
.UseEmojiAndSmiley()
|
||||
.Build()
|
||||
let parsePost (postText:string, fileName:FileInfo, postSettings:PostSettings): Post =
|
||||
let mutable images = System.Collections.Generic.List<string>()
|
||||
let markdownBuilder = getMarkdownBuilder(images)
|
||||
//todo this function is a bit gross
|
||||
let markdownText = postText.Split("---") |> Seq.skip 1 |> String.concat ""
|
||||
let postContent = Markdown.ToHtml(markdownText, markdownBuilder);
|
||||
let postContentPlain = Markdown.ToPlainText(markdownText, markdownBuilder).Split("<!-- more -->") |> String.concat ""
|
||||
//todo pattern match
|
||||
let resolvedUrl = if System.String.IsNullOrWhiteSpace(postSettings.permalink) then fileName.Name.Split('.').[0].Replace(' ', '-').WithoutSpecialCharacters() else postSettings.permalink
|
||||
let summary = postContent.Split("<!-- more -->").[0];
|
||||
let postSummaryPlain = postContentPlain.Split("<!-- more -->").[0];
|
||||
let tags = match postSettings.tags with
|
||||
| null -> Seq.empty
|
||||
| x -> x |> Seq.map fixTagName
|
||||
let summaryPlainShort = match postContentPlain with
|
||||
| postContentPlain when postContentPlain.Length <= 147 -> postContentPlain
|
||||
| postContentPlain -> postContentPlain.Substring(0, 146) + "..."
|
||||
let mapImgUrlFromResolved = mapImgUrlResolver resolvedUrl
|
||||
let images = images |> Seq.distinct |> Seq.map mapImgUrlFromResolved |> Seq.toList
|
||||
{
|
||||
PublishDate = postSettings.date.ToUniversalTime();
|
||||
tags = tags |> Seq.toList
|
||||
Title = postSettings.title;
|
||||
Url = resolvedUrl;
|
||||
Content = HtmlString(postContent);
|
||||
Summary = HtmlString(summary);
|
||||
SummaryPlain = postSummaryPlain;
|
||||
SummaryPlainShort = summaryPlainShort;
|
||||
ContentPlain = postContentPlain;
|
||||
Images = images |> Seq.toList
|
||||
}
|
||||
let getAllPosts path =
|
||||
getPosts path
|
||||
|> Seq.map getFileInfo
|
||||
|> Seq.map(Async.map(fun (text, fileInfo) -> (text, fileInfo, parseYml(text))))
|
||||
|> Seq.map(Async.map(parsePost))
|
||||
|> Async.Parallel
|
||||
// todo stop this
|
||||
|> Async.RunSynchronously
|
||||
|> System.Collections.Generic.List
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
17
src/TerribleDev.Blog.Core/Models/Post.fs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace TerribleDev.Blog.Core.Models
|
||||
open System;
|
||||
open Microsoft.AspNetCore.Html;
|
||||
|
||||
type Post =
|
||||
{
|
||||
Url: string;
|
||||
Title: string;
|
||||
PublishDate: DateTime;
|
||||
Content: HtmlString;
|
||||
Summary: HtmlString;
|
||||
ContentPlain: string;
|
||||
SummaryPlain: string;
|
||||
SummaryPlainShort: string;
|
||||
tags : List<string>
|
||||
Images: List<string>
|
||||
}
|
||||
18
src/TerribleDev.Blog.Core/Models/PostSettings.fs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace TerribleDev.Blog.Core.Models
|
||||
open System;
|
||||
open System.Collections.Generic
|
||||
|
||||
[<CLIMutable>]
|
||||
type PostSettings =
|
||||
{
|
||||
tags: List<string>;
|
||||
title: string
|
||||
permalink: string
|
||||
date: DateTime
|
||||
updated: DateTime
|
||||
id:string
|
||||
thumbnail_image:string
|
||||
thumbnailImage:string
|
||||
thumbnail_image_position:string
|
||||
layout:string
|
||||
}
|
||||
9
src/TerribleDev.Blog.Core/Models/Util.fs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace TerribleDev.Blog.Core.Models
|
||||
open Microsoft.SyndicationFeed
|
||||
open System
|
||||
|
||||
module Util =
|
||||
let ToSyndicationItem (x: Post) =
|
||||
let url = sprintf "https://blog.terribledev.io/%s" x.Url
|
||||
let publishDate : DateTimeOffset = (DateTimeOffset) x.PublishDate
|
||||
SyndicationItem(Title = x.Title, Description = x.Content.ToString(), Id = url, Published = publishDate)
|
||||
23
src/TerribleDev.Blog.Core/TerribleDev.Blog.Core.fsproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Util\fs.fs" />
|
||||
<Compile Include="Util\Constants.fs" />
|
||||
<Compile Include="Util\Async.fs" />
|
||||
<Compile Include="Models\PostSettings.fs" />
|
||||
<Compile Include="Models\Post.fs" />
|
||||
<Compile Include="Models\Util.fs" />
|
||||
<Compile Include="Factories\BlogFactory.fs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Markdig" Version="0.15.7" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.3.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\TerribleDev.Blog.MarkdownPlugins\TerribleDev.Blog.MarkdownPlugins.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
35
src/TerribleDev.Blog.Core/Util/Async.fs
Normal file
@@ -0,0 +1,35 @@
|
||||
module Async
|
||||
|
||||
let map f xAsync = async {
|
||||
// get the contents of xAsync
|
||||
let! x = xAsync
|
||||
// apply the function and lift the result
|
||||
return f x
|
||||
}
|
||||
|
||||
let retn x = async {
|
||||
// lift x to an Async
|
||||
return x
|
||||
}
|
||||
|
||||
let apply fAsync xAsync = async {
|
||||
// start the two asyncs in parallel
|
||||
let! fChild = Async.StartChild fAsync
|
||||
let! xChild = Async.StartChild xAsync
|
||||
|
||||
// wait for the results
|
||||
let! f = fChild
|
||||
let! x = xChild
|
||||
|
||||
// apply the function to the results
|
||||
return f x
|
||||
}
|
||||
|
||||
let bind f xAsync = async {
|
||||
// get the contents of xAsync
|
||||
let! x = xAsync
|
||||
// apply the function but don't lift the result
|
||||
// as f will return an Async
|
||||
return! f x
|
||||
}
|
||||
|
||||
8
src/TerribleDev.Blog.Core/Util/Constants.fs
Normal file
@@ -0,0 +1,8 @@
|
||||
module Constants
|
||||
module Constants=
|
||||
|
||||
let MORE = "<!-- more -->"
|
||||
let YMLDIVIDER = "---"
|
||||
type Tokens =
|
||||
| MORE
|
||||
| YMLDIVIDER
|
||||
10
src/TerribleDev.Blog.Core/Util/fs.fs
Normal file
@@ -0,0 +1,10 @@
|
||||
module fs
|
||||
|
||||
open System.IO
|
||||
|
||||
let getFileInfo (filePath:string) =
|
||||
let fileInfo = FileInfo(filePath)
|
||||
async {
|
||||
let! text = File.ReadAllTextAsync(fileInfo.FullName) |> Async.AwaitTask
|
||||
return (text, fileInfo)
|
||||
}
|
||||
@@ -12,10 +12,10 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
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)
|
||||
public static List<TerribleDev.Blog.Core.Models.Post> postsAsList = TerribleDev.Blog.Core.Factories.BlogFactory.getAllPosts("Posts").OrderByDescending(a => a.PublishDate).ToList();
|
||||
public static Dictionary<string, List<TerribleDev.Blog.Core.Models.Post>> tagToPost = postsAsList.Where(a=>a.tags != null)
|
||||
.Aggregate(
|
||||
new Dictionary<string, List<IPost>>(),
|
||||
new Dictionary<string, List<TerribleDev.Blog.Core.Models.Post>>(),
|
||||
(accum, item) => {
|
||||
foreach(var tag in item.tags)
|
||||
{
|
||||
@@ -25,19 +25,19 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
accum[tag] = new List<IPost>() { item };
|
||||
accum[tag] = new List<TerribleDev.Blog.Core.Models.Post>() { 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) =>
|
||||
public static IDictionary<string, TerribleDev.Blog.Core.Models.Post> posts = postsAsList.ToDictionary(a => a.Url);
|
||||
public static IDictionary<int, List<TerribleDev.Blog.Core.Models.Post>> postsByPage = postsAsList.Aggregate(new Dictionary<int, List<TerribleDev.Blog.Core.Models.Post>>() { [1] = new List<TerribleDev.Blog.Core.Models.Post>() }, (accum, item) =>
|
||||
{
|
||||
var highestPage = accum.Keys.Max();
|
||||
var current = accum[highestPage].Count;
|
||||
if (current >= 10)
|
||||
{
|
||||
accum[highestPage + 1] = new List<IPost>() { item };
|
||||
accum[highestPage + 1] = new List<TerribleDev.Blog.Core.Models.Post>() { item };
|
||||
return accum;
|
||||
}
|
||||
accum[highestPage].Add(item);
|
||||
@@ -51,7 +51,7 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
{
|
||||
if(!postsByPage.TryGetValue(pageNumber, out var result))
|
||||
{
|
||||
return NotFound();
|
||||
return Redirect("/404/");
|
||||
}
|
||||
return View(new HomeViewModel() { Posts = result, Page = pageNumber, HasNext = postsByPage.ContainsKey(pageNumber + 1), HasPrevious = postsByPage.ContainsKey(pageNumber - 1) });
|
||||
}
|
||||
@@ -61,6 +61,7 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
return View(model: postName);
|
||||
}
|
||||
[Route("/offline")]
|
||||
[Route("/offline.html")]
|
||||
[ResponseCache(Duration = 3600)]
|
||||
public IActionResult Offline()
|
||||
{
|
||||
@@ -80,7 +81,7 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
{
|
||||
if(!posts.TryGetValue(postUrl, out var currentPost))
|
||||
{
|
||||
return NotFound();
|
||||
return Redirect("/404/");
|
||||
}
|
||||
return View(model: currentPost);
|
||||
}
|
||||
@@ -88,13 +89,23 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult Error()
|
||||
{
|
||||
this.Response.StatusCode = 500;
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
}
|
||||
[Route("/404")]
|
||||
[Route("{*url}", Order = 999)]
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult FourOhFour()
|
||||
{
|
||||
this.Response.StatusCode = 404;
|
||||
return View();
|
||||
}
|
||||
[Route("/404.html")]
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult FourOhFourCachePage()
|
||||
{
|
||||
//make a route so the service worker can cache a 404 page, but get a valid status code
|
||||
return View(viewName: nameof(FourOhFour));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
public class SeoController : Controller
|
||||
{
|
||||
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();
|
||||
public static IEnumerable<SyndicationItem> postsToSyndication = HomeController.postsAsList.Select(Core.Models.Util.ToSyndicationItem).ToList();
|
||||
[Route("/rss")]
|
||||
[Route("/rss.xml")]
|
||||
[ResponseCache(Duration = 7200)]
|
||||
@@ -48,14 +48,14 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
{
|
||||
Response.StatusCode = 200;
|
||||
Response.ContentType = "text/xml";
|
||||
var sitewideLinks = new List<SiteMapItem>()
|
||||
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 = HomeController.postsAsList.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);
|
||||
|
||||
@@ -9,11 +9,13 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
public class TagsController : Controller
|
||||
{
|
||||
[Route("/all-tags")]
|
||||
[OutputCache(Duration = 31536000)]
|
||||
public IActionResult AllTags()
|
||||
{
|
||||
return View(HomeController.tagToPost);
|
||||
}
|
||||
[Route("/tag/{tagName}")]
|
||||
[OutputCache(Duration = 31536000, VaryByParam = "tagName")]
|
||||
public IActionResult GetTag(string tagName)
|
||||
{
|
||||
if(!HomeController.tagToPost.TryGetValue(tagName, out var models))
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
title: Hosting your blog on the cheap
|
||||
date: 2018-08-22 04:49:46
|
||||
tags:
|
||||
- cloud
|
||||
|
||||
---
|
||||
|
||||
A load of people have been asking me lately how I host my blog. Incase its not apparent, I make 0 dollars on this blog. I refuse to place ads on the page, just to gain pennies of revenue. I do this, not because I don't feel like I shouldn't get paid, but simply because I find ads to be disruptive to the reader. At the end of the day, blogs should have a high signal to noise ratio.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Since I make no money, on this my strategy is about cutting costs. My grandfather use to say "take care of the pounds, because the pennies will take care of themselves." Now since my grandfather is in England, and their dollar is known as the pound, he was telling me to focus on the bigger picture.
|
||||
|
||||
The first big decision for blogs is what "engine" you are going to use, or if you are going to make your own. These usually fall into 2 categories. Static sites, which are usually when blogs are written in text files, and are compiled into static html, or server rendered blogs such as wordpress. When a request is made to blog that has server rendering, the html is dynamically built in time and delivered to the consumer. Static sites, on the other hand are precomputed and thus are just delivered to the browser.
|
||||
|
||||
I won't go into the details on what is better for different scenarios. If you are being cheap, then you will want to use static sites. Static sites are precomputed, which essentially means you just need to serve files to the user. There is no dynamic server to host, you won't need a database, etc. There are a few I like. This blog is ran off [Hexo](https://hexo.io)
|
||||
|
||||
|
||||
|
||||
<!-- So I know what you are thinking, static sites are just 'better' for page load time. While this is true, they can lack dynamic features that might be important to you, such as adding new blog posts on a schedule, or limiting ip addresses, or even some kind of login/subscription model. -->
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
title: Hosting your webapp on the cheap
|
||||
date: 2018-08-22 05:11:20
|
||||
tags:
|
||||
- cloud
|
||||
---
|
||||
|
||||
|
||||
So many people have asked me how I've hosted apps in the past. There is a bit of an art at the moment to making your apps extremely cheap in the cloud. I've heard of hosting costs cut from thousands to pennies.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## Hosting
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
title: 'I used ask.com for 30 days, and this is what I learned'
|
||||
tags:
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
title: Migrating from azure web app to containers
|
||||
tags:
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
title: Precompiling razor views in dotnet core
|
||||
tags:
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
title: Securing your dotnet core apps with hardhat
|
||||
tags:
|
||||
---
|
||||
@@ -0,0 +1,16 @@
|
||||
title: The ultimate chaos monkey. When your cloud provider goes down!
|
||||
date: 2017-03-13 15:20:14
|
||||
tags:
|
||||
- amazon
|
||||
- aws
|
||||
- cloud
|
||||
- DevOps
|
||||
---
|
||||
|
||||
A few weeks ago, the internet delt with the fallout that was [the aws outage](https://techcrunch.com/2017/02/28/amazon-aws-s3-outage-is-breaking-things-for-a-lot-of-websites-and-apps/). AWS, or Amazon Web Services is amazon's cloud platform, and the most popular one to use. There are other platforms similar in scope such as Microsoft's Azure. Amazon had an S3 outage, that ultimately caused other services to fail in the most popular, and oldest region they own. The region dubbed `us-east-1` which is in Virgina.
|
||||
|
||||
This was one of the largest cloud outages we have seen, and users of the cloud found out first hand that the cloud is imperfect. In short when you are using the cloud, you are using services, and infrastructure developed by human beings. However most people turn to tools such as cloud vendors, since the scope of their applications do not, and should not include management of large infrastructure.
|
||||
|
||||
The Netflix, and amazon's of the world are large. Really large, and total avalibility is not just a prefered option, but a basic requirement. Companies that are huge users of the cloud, have started to think about region level depenencies. In short, for huge companies, being in one region is perilous, and frought with danger.
|
||||
|
||||
Infact this isn't the first time we have heard such things. In 2013 Netflix published [an article](http://techblog.netflix.com/2013/05/denominating-multi-region-sites.html) describing how they run in multiple regions. There is an obvious cost in making something work multi-region. This is pretty much for the large companies, however if you are a multi billion dollar organization, working multi-region would probably be an awesome idea.
|
||||
0
src/TerribleDev.Blog.Web/Drafts/keep.md
Normal file
@@ -1,23 +0,0 @@
|
||||
using Microsoft.SyndicationFeed;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public static class IPostExtensions
|
||||
{
|
||||
public static SyndicationItem ToSyndicationItem(this IPost x)
|
||||
{
|
||||
return new SyndicationItem()
|
||||
{
|
||||
Title = x.Title,
|
||||
Description = x.ContentPlain,
|
||||
Id = $"https://blog.terribledev.io/{x.Url}",
|
||||
Published = x.PublishDate
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public static class StringExtension
|
||||
{
|
||||
public static string WithoutSpecialCharacters(this string str)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (char c in str)
|
||||
{
|
||||
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '-')
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
using YamlDotNet.Serialization;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Markdig;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public class BlogFactory
|
||||
{
|
||||
public List<IPost> GetAllPosts()
|
||||
{
|
||||
// why didn't I use f# I'd have a pipe operator by now
|
||||
var posts = GetPosts();
|
||||
var allPosts = posts.AsParallel().Select(a =>
|
||||
{
|
||||
var fileInfo = new FileInfo(a);
|
||||
var fileText = File.ReadAllText(fileInfo.FullName);
|
||||
return ParsePost(fileText, fileInfo.Name);
|
||||
});
|
||||
return allPosts.ToList();
|
||||
}
|
||||
public IEnumerable<string> GetPosts() => Directory.EnumerateFiles(Path.Combine(Directory.GetCurrentDirectory(), "Posts"), "*.md", SearchOption.TopDirectoryOnly);
|
||||
|
||||
public PostSettings ParseYaml(string ymlText)
|
||||
{
|
||||
var serializer = new DeserializerBuilder().Build();
|
||||
return serializer.Deserialize<PostSettings>(ymlText);
|
||||
|
||||
}
|
||||
public IPost ParsePost(string postText, string fileName)
|
||||
{
|
||||
var splitFile = postText.Split("---");
|
||||
var ymlRaw = splitFile[0];
|
||||
var markdownText = string.Join("", splitFile.Skip(1));
|
||||
var pipeline = new MarkdownPipelineBuilder().UseEmojiAndSmiley().Build();
|
||||
var postContent = Markdown.ToHtml(markdownText, pipeline);
|
||||
var postContentPlain = String.Join("", Markdown.ToPlainText(markdownText, pipeline).Split("<!-- more -->"));
|
||||
var postSettings = ParseYaml(ymlRaw);
|
||||
var resolvedUrl = !string.IsNullOrWhiteSpace(postSettings.permalink) ? postSettings.permalink : fileName.Split('.')[0].Replace(' ', '-').WithoutSpecialCharacters();
|
||||
var summary = postContent.Split("<!-- more -->")[0];
|
||||
var postSummaryPlain = postContentPlain.Split("<!-- more -->")[0];
|
||||
return new Post()
|
||||
{
|
||||
PublishDate = postSettings.date,
|
||||
tags = postSettings.tags?.Select(a=>a.Replace(' ', '-').WithoutSpecialCharacters().ToLower()).ToList() ?? new List<string>(),
|
||||
Title = postSettings.title,
|
||||
Url = resolvedUrl,
|
||||
Content = new HtmlString(postContent),
|
||||
Summary = new HtmlString(summary),
|
||||
SummaryPlain = postSummaryPlain,
|
||||
ContentPlain = postContentPlain
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public interface IBlogFactory
|
||||
{
|
||||
Task<IEnumerable<IPost>> GetAllPosts();
|
||||
IEnumerable<string> GetPosts();
|
||||
IPost ParsePost(string postText, string fileName);
|
||||
IPostSettings ParseYaml(string ymlText);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class GetTagViewModel
|
||||
{
|
||||
public IEnumerable<IPost> Posts { get; set; }
|
||||
public IEnumerable<TerribleDev.Blog.Core.Models.Post> Posts { get; set; }
|
||||
public string Tag { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class HomeViewModel
|
||||
{
|
||||
public IEnumerable<IPost> Posts { get; set;}
|
||||
public IEnumerable<TerribleDev.Blog.Core.Models.Post> Posts { get; set;}
|
||||
public int Page { get; set; }
|
||||
public string NextUrl { get; set; }
|
||||
public string PreviousUrl { get; set; }
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using YamlDotNet.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public interface IPost
|
||||
{
|
||||
string Url { get; set; }
|
||||
string Title { get; set; }
|
||||
HtmlString Summary { get; set; }
|
||||
DateTime PublishDate { get; set; }
|
||||
HtmlString Content { get; set; }
|
||||
string ContentPlain { get; set; }
|
||||
string SummaryPlain { get; set; }
|
||||
IList<string> tags { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public interface IPostSettings
|
||||
{
|
||||
string id { get; set; }
|
||||
List<string> tags { get; set; }
|
||||
string title { get; set; }
|
||||
string permalink { get; set; }
|
||||
string thumbnailImage { get; set; }
|
||||
DateTimeOffset date { get; set; }
|
||||
DateTimeOffset updated { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class Post : IPost
|
||||
{
|
||||
public string Url { get; set; }
|
||||
public string Title { get; set; }
|
||||
public DateTime PublishDate { get; set; }
|
||||
public HtmlString Content { get; set; }
|
||||
public HtmlString Summary { get; set; }
|
||||
public string ContentPlain { get; set; }
|
||||
public string SummaryPlain { get; set; }
|
||||
public IList<string> tags { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class PostModel
|
||||
{
|
||||
public HtmlString Content { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class PostSettings
|
||||
{
|
||||
public List<string> tags { get; set; }
|
||||
public string title { get; set; }
|
||||
public string permalink { get; set; }
|
||||
public DateTime date { get; set; }
|
||||
public DateTime updated { get; set; }
|
||||
public string id { get; set; }
|
||||
public string thumbnail_image { get; set; }
|
||||
public string thumbnailImage { get; set; }
|
||||
public string thumbnail_image_position { get; set; }
|
||||
public string layout { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using System.Xml.Serialization;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
[XmlRoot("urlset")]
|
||||
[XmlRoot("urlset", Namespace="http://www.sitemaps.org/schemas/sitemap/0.9")]
|
||||
public class SiteMapRoot
|
||||
{
|
||||
[XmlElement("url")]
|
||||
|
||||
@@ -3,6 +3,7 @@ permalink: anti-forgery-tokens-in-nancyfx-with-razor
|
||||
id: 33
|
||||
updated: '2014-06-11 20:00:34'
|
||||
date: 2014-06-11 19:34:13
|
||||
tags:
|
||||
---
|
||||
|
||||
Getting started with anti-forgery tokens in NancyFX with razor views is pretty simple.
|
||||
|
||||
@@ -7,7 +7,7 @@ tags:
|
||||
- docker
|
||||
---
|
||||
|
||||
Here we are, its 2017 dotnet core is out, and finally dotnet has a proper cli. In a previous post [we explored the new cli](http://blog.terribledev.io/Exploring-the-dotnet-cli/). In short you can use the dotnet cli to build, test, package, and publish projects. However sometimes just using the cli is not enough. Sometimes, you land in a place where you have many projects to compile, test, and package.
|
||||
Here we are, its 2017 dotnet core is out, and finally dotnet has a proper cli. In a previous post [we explored the new cli](/Exploring-the-dotnet-cli/). In short you can use the dotnet cli to build, test, package, and publish projects. However sometimes just using the cli is not enough. Sometimes, you land in a place where you have many projects to compile, test, and package.
|
||||
<!-- more -->
|
||||
|
||||
You sometimes need a more complex tool to help you manage your versions, and set the right properties as part of your builds. This is where a tasking system like [gulp](http://gulpjs.com/) can help. Now gulp is not the only task engines. There are Rake, Cake, MSBuild, etc. Plenty to pick from. I personally use gulp a lot, because I'm a web developer. I need a JS based system, to help me run the [babels](https://babeljs.io), and [webpacks](https://webpack.github.io/docs/) of the world.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
title: Compressing images with tinypng's CLI
|
||||
date: 2019-01-23 10:50
|
||||
tags:
|
||||
- javascript
|
||||
- tools
|
||||
---
|
||||
|
||||
Ok so I'm really lazy, and I honestly think that has helped me a lot in this industry. I always try to work smarter, not harder. I take many screen shots for this blog, and I need to optimize them. Incase you didn't know many images are often larger than they need to be slowing the download time. However, I don't ever want to load them into photoshop. Too much time and effort!
|
||||
|
||||
|
||||
<!-- more -->
|
||||
|
||||
At first I tried to compress images locally, but it took to long to run through all the images I had. So recently I started using a service called [tiny png](https://tinypng.com/) to compress images. Now the website seems to indicate that you upload images, and you will get back optimized versions. However to me this takes too much time. I don't want the hassle of zipping my images uploading them, downloading the results. Again, lazy!
|
||||
|
||||
So I figured out they have a cli in npm. Easy to install, just use npm to globally install it. `npm install -g tinypng-cli`.
|
||||
|
||||
Now you have to call the cli, this is the flags I use `tinypng . -r -k YourKeyHere`. The period tells tinypng to look in the current directory for images, `-r` tells it to look recursively, or essentially to look through child directories as well, and the `-k YourKeyHere` is the key you get by logging in. On the free plan you get 500 compressions a month. Hopefully you will fall into the pit of success like I did!
|
||||
|
||||

|
||||
@@ -3,6 +3,7 @@ permalink: fixing-could-not-load-file-or-assembly-microsoft-dnx-host-clr-2
|
||||
id: 53
|
||||
updated: '2015-09-09 17:34:41'
|
||||
date: 2015-09-09 10:08:18
|
||||
tags:
|
||||
---
|
||||
|
||||
So I recently ran into this error where the latest bits could not load Microsoft.Dnx.Host.Clr here is what I did to fix it.
|
||||
|
||||
@@ -8,5 +8,4 @@ I put together [some materials](https://github.com/TerribleDev/intro-to-docker)
|
||||
<br />
|
||||
|
||||
<!--more-->
|
||||
|
||||
{% youtube 6EGyhDlr8rs %}
|
||||

|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
title: Rebuilding this blog for performance
|
||||
date: 2019-01-21 17:56:34
|
||||
tags:
|
||||
- performance
|
||||
- battle of the bulge
|
||||
- javascript
|
||||
- dotnet
|
||||
---
|
||||
|
||||
So many people know me as a very performance focused engineer, and as someone that cares about perf I've always been a bit embarrassed about this blog. In actual fact this blog as it sits now is **fast** by most people's standards. I got a new job in July, and well I work with an [absolute mad lad](https://twitter.com/markuskobler) that is making me feel pretty embarrassed with his 900ms page load times. So I've decided to build my own blog engine, and compete against him.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## Approach
|
||||
|
||||
Ok, so I want a really fast blog, but one that does not sacrifice design. I plan to pre-compute the HTML into memory, but I am not going to serve static files. In this case, I'll need an application server. I'm going to have my own CSS styles, but I'm hoping to be in the (almost) no-JS camp. Not that I dislike JS, but I want to do as much pre-computing as possible, and I don't want to slow the page down with compute in the client.
|
||||
|
||||
## Features
|
||||
|
||||
This blog has a view to read a post. A home page with links to the last 10 blog posts and a pager to go back further in time. A page listing blogs by tags and links for each tag to posts.
|
||||
|
||||
## Picking Technologies
|
||||
|
||||
So in the past my big philosophy has been that most programming languages and technologies really don't matter for most applications. In fact this use-case *could* and probably should be one of them, but when you go to extremes that I go, you want to look at benchmarks. [Tech empower](https://www.techempower.com/benchmarks/) does benchmarks of top programming languages and frameworks. For my blog since it will be mostly be bytes in bytes out, precomputed, we should look at the plain text benchmark. The top 10 webservers include go, java, rust, c++, and C#. Now I know rust, go and C# pretty well. Since the rust, and go webservers listed in the benchmark were mostly things no one really uses, I decided to use dotnet. This is also for a bit of a laugh, because my competition hates dotnet, and I also have deep dotnet expertise I can leverage.
|
||||
|
||||
|
||||
## Server-side approach
|
||||
|
||||
So as previously mentioned we'll be precomputing blog posts. I plan to compute the posts and hand them down to the views. If we use completely immutable data structures we'll prevent any locking that could slow down our app.
|
||||
|
||||
## ASPNET/Dotnet Gotchas
|
||||
|
||||
So dotnet is a managed language with a runtime. Microsoft has some [performance best practices](https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-2.2), but here are some of my thoughts.
|
||||
|
||||
* There is a tool called [cross gen](https://github.com/dotnet/coreclr/blob/master/Documentation/building/crossgen.md) which compiles dll's to native code.
|
||||
* Dotnet's garbage collector is really good, but it struggles to collect long living objects. Our objects will need to either be ephemeral, or pinned in memory forever.
|
||||
* The garbage collector struggles with large objects, especially large strings. We'll have to avoid large string allocations when possible.
|
||||
* dotnet has reference types such as objects, classes, strings, and most other things are value types. [Value types are allocated](/c-strings/) on the stack which is far cheaper than the heap
|
||||
* Exceptions are expensive when thrown in dotnet. I'm going to always avoid hitting them.
|
||||
* Cache all the things!
|
||||
|
||||
In the past we had to pre-compile razor views, but in 2.x of dotnet core, that is now built in. So one thing I don't have to worry about
|
||||
|
||||
|
||||
## Client side page architecture and design
|
||||
|
||||
So here are my thoughts on the client side of things.
|
||||
|
||||
* Minify all the content
|
||||
* Fingerprint all css/js content and set cache headers to maximum time
|
||||
* Deliver everything with brotli compression
|
||||
* Zopfli and gzip for fallbacks
|
||||
* Always use `Woff2` for fonts
|
||||
* Avoid expensive css selectors
|
||||
* `:nth child`
|
||||
* `fixed`
|
||||
* partial matching `[class^="wrap"]`
|
||||
* Use HTTP/2 for **all requests**
|
||||
* Images
|
||||
* Use SVG's when possible
|
||||
* Recompile all images in the build to `jpeg 2000, jpeg xr, and webp`
|
||||
* Serve `jpeg 2000` to ios
|
||||
* `jpeg XR` to ie11 and edge
|
||||
* Send `webp` to everyone else
|
||||
* PWA
|
||||
* Use a service worker to cache assets
|
||||
* Also use a service worker to prefetch blog posts
|
||||
* Offline support
|
||||
* CDN
|
||||
* Use Cloudflare to deliver assets faster
|
||||
* Cloudflare's argo improves geo-routing and latency issues
|
||||
* Throw any expected 301's inside cloudflares own datacenters with workers
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
These are the list of tools I'm using to measure performance.
|
||||
|
||||
* `lighthouse` - Built into chrome (its in the audit tab in the devtools), this displays a lot of performance and PWA improvements.
|
||||
* [Web Hint](https://webhint.io/) is like a linter for your web pages. The tool provides a ton of improvements from accessibility to performance
|
||||
* I really like [pingdom's](https://tools.pingdom.com/) page load time tool.
|
||||
* Good ol' [web page test is also great](https://www.webpagetest.org/)
|
||||
* The chrome devtools can also give you a breakdown as to what unused css you have on the page
|
||||
@@ -16,8 +16,7 @@ Today marks the release of Visual Studio 2017, and with it the final release of
|
||||
|
||||
So I bet you are wondering, how is VS2017 improved. When you first boot the vs2017 installer you are immediately hit with a very sleek UI for the installer. The installer actually has reasonable install sizes for scenarios like nodejs only.
|
||||
|
||||
|
||||
{% image "fancybox" vs.PNG "vs 2017 installer" %}
|
||||

|
||||
|
||||
VS2017 can understand which lines of code are linked to your unit tests. As you alter, or refactor code VS can run the tests. This can allow the editor to show checkmarks or red `x`'s This is huge as it can seemingly provide constant feedback to developers during development.
|
||||
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
title: The battle of the buldge. Visualizing your javascript bundle
|
||||
title: The battle of the bulge. Visualizing your javascript bundle
|
||||
date: 2018-10-17 13:19:18
|
||||
tags:
|
||||
- javascript
|
||||
- battle of the bulge
|
||||
- performance
|
||||
---
|
||||
|
||||
So incase you havn't been following me. I joined Cargurus in July. At cargurus we're currently working on our mobile web experience written in react, redux and reselect. As our implementation grew so did our time to first paint.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
So I've been spending a lot of time working on our performance. One tool I have found invaluable in the quest for page perf mecca is [source-map-explorer](https://www.npmjs.com/package/source-map-explorer). This is a tool that dives into a bundled file, and its map. Then visualizes the bundle in a tree view. This view lets you easily understand exactly what is taking up space in the bundle. What I love about this tool is that it works with any type of bundled javascript file, and is completely seperate of the build. So any bugs in webpack where you have duplicate files in a bundle will appear here.
|
||||
So I've been spending a lot of time working on our performance. One tool I have found invaluable in the quest for page perf mecca is [source-map-explorer](https://www.npmjs.com/package/source-map-explorer). This is a tool that dives into a bundled file, and its map. Then visualizes the bundle in a tree view. This view lets you easily understand exactly what is taking up space in the bundle. What I love about this tool is that it works with any type of bundled javascript file, and is completely de-void of any builds. So any bugs in your webpack config leading to duplicate files in a bundle will show up here.
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
You get started by `npm install -g source-map-explorer` then just download your bundles, and sourcemaps. In the command line run `source-map-explorer ./yourbundle.js ./yourbundlemap.js` Your browser should then open with a great tree view of what is inside your bundle. From here you can look to see what dependencies you have, and their sizes. Obviously, you can then decide to keep or throw them away.
|
||||
You get started by `npm install -g source-map-explorer` then just download your bundles, and sourcemaps. You can do this from production if you have them. Otherwise build bundles locally. **Note** You should always use this on minified code where any tree shaking and dead code elimination has occurred. In the command line run `source-map-explorer ./yourbundle.js ./yourbundle.js.map` Your browser should then open with a great tree view of what is inside your bundle. From here you can look to see what dependencies you have, and their sizes. Obviously, you can then decide to keep or throw them away.
|
||||
|
||||

|
||||
|
||||
Here is a great youtube video explaining it in detail!
|
||||
|
||||
|
||||
{% youtube 7aY9BoMEpG8 %}
|
||||

|
||||
@@ -0,0 +1,54 @@
|
||||
title: 'Measuring, Visualizing and Debugging your React Redux Reselect performance bottlenecks'
|
||||
date: 2019-01-14 22:04:56
|
||||
tags:
|
||||
- battle of the bulge
|
||||
- javascript
|
||||
- performance
|
||||
---
|
||||
|
||||
In the battle of performance one tool constantly rains supreme, the all powerful profiler! In javascript land chrome has a pretty awesome profiler, but every-time I looked into our react perf issues I was always hit by a slow function called `anonymous function`
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## Using the chrome profiler
|
||||
|
||||
So if you open the chrome devtools, you will see a tab called `performance`. Click on that tab. If you are looking into CPU bound workloads click the CPU dropdown and set yourself to 6x slowdown, which will emulate a device that is much slower.
|
||||
|
||||

|
||||
|
||||
Press the record button, click around on your page, then click the record button again. You are now hit with a timeline of your app, and what scripts were ran during this time.
|
||||
|
||||
So what I personally like to do is find orange bars that often make up the bulk of the time. However I've often noticed the bulk of bigger redux apps are taken up by `anonymous functions` or functions that essentially have no name. They often look like this `() => {}`. This is largely because they are inside of [reselect selectors](https://github.com/reduxjs/reselect). Incase you are unfamiliar selectors are functions that cache computations off the redux store. Back to the chrome profiler. One thing you can do it use the `window.performance` namespace to measure and record performance metrics into the browser. If you expand the `user timings section` in the chrome profiler you may find that react in dev mode has included some visualizations for how long components take to render.
|
||||
|
||||

|
||||
|
||||
## Adding your own visualizations
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
## So how do I use your gist?
|
||||
|
||||
You can copy the code into a file of your own. If you use reselect you probably have code that looks like the following.
|
||||
|
||||
```js
|
||||
export const computeSomething = createSelector([getState], (state) => { /* compute projection */ });
|
||||
```
|
||||
|
||||
You just need to replace the above with the following
|
||||
|
||||
```js
|
||||
export const computeSomething = createMarkedSelector('computeSomething', [getState], (state) => { /* compute projection */ });
|
||||
```
|
||||
|
||||
its pretty simple, it just requires you to pass a string in the first argument slot. That string will be the name used to write to the performance API, and will show up in the chrome profiler. Inside vscode you can even do a regex find and replace to add this string.
|
||||
|
||||
|
||||
```
|
||||
find: const(\s?)(\w*)(\s?)=(\s)createSelector\(
|
||||
|
||||
replace: const$1$2$3=$4createMarkedSelector('$2',
|
||||
```
|
||||
@@ -52,14 +52,9 @@ namespace TerribleDev.Blog.Web
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts(TimeSpan.FromDays(30), false, preload: true);
|
||||
|
||||
}
|
||||
app.UseIENoOpen();
|
||||
app.UseNoMimeSniff();
|
||||
app.UseCrossSiteScriptingFilters();
|
||||
app.UseFrameGuard(new FrameGuardOptions(FrameGuardOptions.FrameGuard.SAMEORIGIN));
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseResponseCompression();
|
||||
|
||||
@@ -82,6 +77,11 @@ namespace TerribleDev.Blog.Web
|
||||
}
|
||||
});
|
||||
app.UseRewriter(new Microsoft.AspNetCore.Rewrite.RewriteOptions().AddRedirect("(.*[^/|.xml|.html])$", "$1/", 301));
|
||||
app.UseIENoOpen();
|
||||
app.UseNoMimeSniff();
|
||||
app.UseCrossSiteScriptingFilters();
|
||||
app.UseFrameGuard(new FrameGuardOptions(FrameGuardOptions.FrameGuard.SAMEORIGIN));
|
||||
app.UseHsts(TimeSpan.FromDays(30), false, preload: true);
|
||||
app.UseOutputCaching();
|
||||
app.UseMvc();
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Taghelpers
|
||||
{
|
||||
[HtmlTargetElement("desktopOnly", TagStructure = TagStructure.NormalOrSelfClosing)]
|
||||
public class DesktopTagHelper : TagHelper
|
||||
{
|
||||
static Regex MobileCheck = new Regex(@"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
|
||||
static ConcurrentDictionary<string, bool> CachedChecks = new ConcurrentDictionary<string, bool>();
|
||||
public string UserAgent { get; set; }
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
output.TagName = null;
|
||||
if (string.IsNullOrEmpty(UserAgent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var shouldRender = true;
|
||||
if(CachedChecks.TryGetValue(UserAgent, out var cacheResult))
|
||||
{
|
||||
shouldRender = cacheResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
var isMobile = MobileCheck.IsMatch(UserAgent);
|
||||
shouldRender = !isMobile;
|
||||
CachedChecks.TryAdd(UserAgent, !isMobile);
|
||||
}
|
||||
if(!shouldRender)
|
||||
{
|
||||
output.SuppressOutput();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,12 @@
|
||||
<Content Include="Posts\*.md" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\TerribleDev.Blog.MarkdownPlugins\TerribleDev.Blog.MarkdownPlugins.csproj" />
|
||||
<ProjectReference Include="..\TerribleDev.Blog.Core\TerribleDev.Blog.Core.fsproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Factories\BlogFactory.cs" />
|
||||
<Compile Remove="Extensions\IPostExtensions.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "FourOhFour";
|
||||
ViewData["DisableHeader"] = true;
|
||||
}
|
||||
|
||||
<h1>Ruh Oh!</h1>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Home Page";
|
||||
ViewData["DisableHeader"] = true;
|
||||
}
|
||||
<cache vary-by-route="pageNumber">
|
||||
|
||||
@@ -10,14 +9,17 @@
|
||||
{
|
||||
<partial name="PostSummary" model="post" />
|
||||
}
|
||||
@if (Model.HasNext)
|
||||
{
|
||||
<div class="bottomNavButtons">
|
||||
<a href="/page/@(Model.Page - 1)/" class="btn">← Previous Page</a>
|
||||
@if (Model.HasPrevious)
|
||||
{
|
||||
<a href="/page/@(Model.Page - 1)/" class="btn">← Previous Page</a>
|
||||
}
|
||||
<div class="spacer"></div>
|
||||
<a href="/page/@(Model.Page + 1)/" class="btn">Next Page →</a>
|
||||
@if (Model.HasNext)
|
||||
{
|
||||
<a href="/page/@(Model.Page + 1)/" class="btn">Next Page →</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</cache>
|
||||
|
||||
@section Head {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@model IPost
|
||||
@model TerribleDev.Blog.Core.Models.Post
|
||||
@{
|
||||
ViewData["Title"] = "Post";
|
||||
ViewData["HideNav"] = true;
|
||||
@@ -9,17 +9,24 @@
|
||||
</cache>
|
||||
|
||||
@section Head {
|
||||
<meta name="description" content="@Model.SummaryPlain" />
|
||||
<meta name="description" content="@Model.SummaryPlainShort" />
|
||||
<meta property="og:type" content="blog">
|
||||
<meta property="og:title" content="@Model.Title">
|
||||
<meta property="og:url" content="https://blog.terribledev.io/Visualizing-your-react-redux-performance-bottlenecks/index.html">
|
||||
<meta property="og:url" content="https://blog.terribledev.io/@Model.Url/">
|
||||
<meta property="og:site_name" content="The Ramblings of TerribleDev">
|
||||
<meta property="og:description" content="@Model.SummaryPlain">
|
||||
<meta property="og:updated_time" content="2019-01-20T15:07:51.000Z">
|
||||
<meta property="og:description" content="@Model.SummaryPlainShort">
|
||||
<meta property="og:updated_time" content="@Model.PublishDate.ToString("O")">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="@Model.Title">
|
||||
<meta name="twitter:description" content="@Model.SummaryPlain">
|
||||
<meta name="twitter:image" content="https://blog.terribledev.io/1.png">
|
||||
<meta name="twitter:description" content="@Model.SummaryPlainShort">
|
||||
<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="https://blog.terribledev.io@(image)">
|
||||
}
|
||||
@if(Model.Images.Length > 0)
|
||||
{
|
||||
<meta name="twitter:image" content="https://blog.terribledev.io@(Model.Images[0])">
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
@model IPost
|
||||
@model TerribleDev.Blog.Core.Models.Post
|
||||
|
||||
<article itemprop="blogPost">
|
||||
<h1 itemprop="headline" class="headline">@Model.Title</h1>
|
||||
<time class="headlineSubtext" itemprop="datePublished" content="@Model.PublishDate.ToString()">@Model.PublishDate.ToString("D")</time>
|
||||
@Model.Content
|
||||
@if (Model.tags.Count > 0)
|
||||
@if (Model.tags.Length > 0)
|
||||
{
|
||||
<div>
|
||||
<span>Tagged In:</span><br />
|
||||
@foreach (var tag in Model.tags)
|
||||
{
|
||||
<a href="/tag/@tag" class="btn block">@tag</a>
|
||||
<a href="/tag/@tag/" class="btn block">@tag</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
@{
|
||||
var hideNav = ViewData["HideNav"] != null ? "hide" : "";
|
||||
var hideNav = ViewData["HideNav"] != null ? "" : "withBody";
|
||||
}
|
||||
<nav class="navBar @hideNav">
|
||||
@if (ViewData["HideNav"] != null)
|
||||
{
|
||||
<img src="" data-src="~/content/tommyAvatar3.jpg" class="lazy round" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="~/content/tommyAvatar3.jpg" class="round" />
|
||||
}
|
||||
|
||||
<nav class="navBar hide @hideNav" id="navBar">
|
||||
<img src="" alt="An image of TerribleDev" data-src="/content/tommyAvatar4.jpg" class="lazy round" />
|
||||
<span>Tommy "Terrible Dev" Parnell</span>
|
||||
<ul class="sidebarBtns">
|
||||
<li><a href="/" class="link-unstyled">Home</a></li>
|
||||
<li><a href="/all-tags" class="link-unstyled">Tags</a></li>
|
||||
<li><a href="/about" class="link-unstyled">About</a></li>
|
||||
<li><a href="https://github.com/terribledev" target="_blank" class="link-unstyled">Github</a></li>
|
||||
<li><a href="https://twitter.com/terribledev" target="_blank" class="link-unstyled">Twitter</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>
|
||||
<li><a href="https://twitter.com/terribledev" rel="noopener" target="_blank" class="link-unstyled">Twitter</a></li>
|
||||
<li><a href="mailto:tommy@terribledev.io" class="link-unstyled">Email</a></li>
|
||||
<li><span class="link-unstyled" id="closeNav">Close Navbar</span></li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -1,10 +1,10 @@
|
||||
@model IPost
|
||||
@model TerribleDev.Blog.Core.Models.Post
|
||||
|
||||
<article class="btmRule">
|
||||
<h3 itemprop="headline" class="headline"><a href="/@Model.Url" class="link-unstyled">@Model.Title</a></h3>
|
||||
<h3 itemprop="headline" class="headline"><a href="/@Model.Url/" class="link-unstyled">@Model.Title</a></h3>
|
||||
<time class="headlineSubtext" itemprop="datePublished" content="@Model.PublishDate.ToString()">@Model.PublishDate.ToString("D")</time>
|
||||
<div itemprop="articleBody">
|
||||
@Model.Summary
|
||||
</div>
|
||||
<a href="/@Model.Url">Continue Reading </a>
|
||||
<a href="/@Model.Url/">Continue Reading </a>
|
||||
</article>
|
||||
@@ -1,7 +1,7 @@
|
||||
<meta name="description" content="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." />
|
||||
<meta property="og:type" content="blog">
|
||||
<meta property="og:title" content="The Ramblings of TerribleDev">
|
||||
<meta property="og:url" content="https://blog.terribledev.io/index.html">
|
||||
<meta property="og:url" content="https://blog.terribledev.io/">
|
||||
<meta property="og:site_name" content="The Ramblings of TerribleDev">
|
||||
<meta property="og:description" content="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.">
|
||||
<meta name="twitter:card" content="summary">
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<partial name="Gtm" />
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
|
||||
<partial name="Gtm" />
|
||||
<meta name="author" content="Tommy "TerribleDev" Parnell" />
|
||||
<meta name="theme-color" content="#4A4A4A" />
|
||||
<link rel="alternate" type="application/atom+xml" title="RSS" href="/rss.xml" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="alternate" type="application/atom+xml" title="RSS" href="/rss.xml">
|
||||
<link rel="manifest" href="~/manifest.json" asp-append-version="true">
|
||||
<link asp-append-version="true" rel="icon" href="~/favicon.ico" />
|
||||
<link rel="alternate" type="application/atom+xml" async title="RSS" href="/rss.xml">
|
||||
<link rel="manifest" href="~/manifest.json" async asp-append-version="true">
|
||||
<link asp-append-version="true" rel="icon" async href="~/favicon.ico" />
|
||||
<title>@ViewData["Title"] - The Ramblings of TerribleDev</title>
|
||||
<environment names="Development">
|
||||
<link asp-append-version="true" rel="stylesheet" href="~/css/site.css" />
|
||||
@@ -22,39 +20,18 @@
|
||||
</head>
|
||||
<body>
|
||||
<partial name="Nav" />
|
||||
@if (ViewData["DisableHeader"] == null)
|
||||
{
|
||||
<header class="header">
|
||||
<div><a href="/" class="link-unstyled">The Ramblings of TerribleDev</a></div>
|
||||
</header>
|
||||
}
|
||||
<header class="header">
|
||||
<svg aria-label="Open Menu" id="menuBtn" role="button" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M4 10h24c1.104 0 2-.896 2-2s-.896-2-2-2H4c-1.104 0-2 .896-2 2s.896 2 2 2zm24 4H4c-1.104 0-2 .896-2 2s.896 2 2 2h24c1.104 0 2-.896 2-2s-.896-2-2-2zm0 8H4c-1.104 0-2 .896-2 2s.896 2 2 2h24c1.104 0 2-.896 2-2s-.896-2-2-2z" /></svg>
|
||||
<div class="headerCallout"><a href="/" class="link-unstyled ">The Ramblings of TerribleDev</a></div>
|
||||
</header>
|
||||
@{
|
||||
var bodyBump = ViewData["HideNav"] == null ? "bodyWithNav": "";
|
||||
var headerBump = ViewData["DisableHeader"] == null ? "headerBump" : "";
|
||||
}
|
||||
<main role="main" class="@bodyBump @headerBump">
|
||||
<main role="main" class="@bodyBump headerBump">
|
||||
<div class="main-content-wrap">
|
||||
@RenderBody()
|
||||
</div>
|
||||
</main>
|
||||
@*@if (ViewData["DisableHeader"] != null)
|
||||
{
|
||||
|
||||
<main role="main" class="bodyWithNav">
|
||||
<div class="main-content-wrap">
|
||||
@RenderBody()
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
else
|
||||
{
|
||||
<header class="header">
|
||||
<div><a href="/" class="link-unstyled">The Ramblings of TerribleDev</a></div>
|
||||
</header>
|
||||
<main role="main" class="main-content-wrap headerBump">
|
||||
@RenderBody()
|
||||
</main>
|
||||
}*@
|
||||
@RenderSection("Scripts", required: false)
|
||||
<environment names="Development">
|
||||
<script asp-append-version="true" src="~/js/swi.js" async></script>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
@model Dictionary<string, List<IPost>>
|
||||
@model Dictionary<string, List<TerribleDev.Blog.Core.Models.Post>>
|
||||
@{
|
||||
ViewData["Title"] = "all-tags";
|
||||
ViewData["DisableHeader"] = true;
|
||||
}
|
||||
<h2>All Tags</h2>
|
||||
@foreach (var tag in Model.Keys)
|
||||
{
|
||||
<a href="/tag/@tag/" class="btn block">@tag</a>
|
||||
}
|
||||
|
||||
<cache>
|
||||
@foreach (var tag in Model.Keys)
|
||||
{
|
||||
<a href="/tag/@tag/" class="btn block">@tag</a>
|
||||
}
|
||||
</cache>
|
||||
@section Head {
|
||||
<partial name="StockMeta" />
|
||||
}
|
||||
@@ -2,9 +2,10 @@
|
||||
@model GetTagViewModel
|
||||
@{
|
||||
ViewData["Tag:" + Model.Tag] = "GetTag";
|
||||
ViewData["DisableHeader"] = true;
|
||||
}
|
||||
<cache vary-by-route="tagName">
|
||||
@foreach (var post in Model.Posts)
|
||||
{
|
||||
<partial name="PostSummary" model="post" />
|
||||
}
|
||||
</cache>
|
||||
@@ -1,4 +1,3 @@
|
||||
@using TerribleDev.Blog.Web
|
||||
@using TerribleDev.Blog.Web.Models
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, TerribleDev.Blog.Web
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
BIN
src/TerribleDev.Blog.Web/wwwroot/content/tommyAvatar4.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -6,6 +6,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||
color: #4A4A4A;
|
||||
line-height: 1.45;
|
||||
letter-spacing: -.01em;
|
||||
line-height: 1.25em
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -46,7 +47,7 @@ body {
|
||||
padding-bottom: 1.2em;
|
||||
}
|
||||
.headerBump {
|
||||
padding-top: 3.5rem;
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.main-content-wrap img {
|
||||
@@ -94,17 +95,99 @@ a {
|
||||
color: #6c6c6c;
|
||||
}
|
||||
|
||||
|
||||
.btmRule {
|
||||
border-bottom: 1px solid #eef2f8;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
.navBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
background: #4A4A4A;
|
||||
color: white;
|
||||
width: 250px;
|
||||
padding-top: 20px;
|
||||
transition: width ease-in-out .5s;
|
||||
z-index: 100;
|
||||
overflow: auto;
|
||||
}
|
||||
.navBar.withBody > * > * > #closeNav {
|
||||
display: none;
|
||||
}
|
||||
.navBar.withBody.hide {
|
||||
width: 250px;
|
||||
}
|
||||
.navBar.hide {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.navBar > * {
|
||||
max-width: 100%;
|
||||
color: white;
|
||||
}
|
||||
.header {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 3.5rem;
|
||||
top: 0;
|
||||
border: 1px solid #eef2f8;
|
||||
color: #4A4A4A;
|
||||
background-color: white;
|
||||
z-index: 20;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.headerCallout {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#menuBtn {
|
||||
cursor: pointer;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.sidebarBtns {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebarBtns > li {
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.bottomNavButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: .5rem;
|
||||
width: 100%;
|
||||
}
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: auto;
|
||||
height: auto;
|
||||
background: #fff !important;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
color: #5d686f !important;
|
||||
color: #5d686f;
|
||||
border: 1px solid #9eabb3;
|
||||
padding: .3em .2em;
|
||||
text-decoration: none !important;
|
||||
text-decoration: none;
|
||||
font-size: 1.1rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -117,10 +200,19 @@ a {
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
color: #4A4A4A !important;
|
||||
border: 1px solid #738691 !important;
|
||||
color: #4A4A4A;
|
||||
border: 1px solid #738691;
|
||||
}
|
||||
|
||||
|
||||
|
||||
a.link-unstyled, div.link-unstyled, span.link-unstyled, svg.link-unstyled {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
@@ -146,82 +238,18 @@ a {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.navBar {
|
||||
width: 100%;
|
||||
}
|
||||
.navBar.withBody.hide {
|
||||
width: 0;
|
||||
}
|
||||
.navBar.withBody > * > * > #closeNav {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
.bodyWithNav {
|
||||
width: initial;
|
||||
float: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.link-unstyled {
|
||||
cursor: pointer !important;
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-weight: inherit !important;
|
||||
}
|
||||
|
||||
.btmRule {
|
||||
border-bottom: 1px solid #eef2f8;
|
||||
}
|
||||
|
||||
.navBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
background: #4A4A4A;
|
||||
color: white;
|
||||
width: 250px;
|
||||
padding-top: 20px;
|
||||
transition: width ease-in .5s;
|
||||
z-index: 100;
|
||||
}
|
||||
.navBar.hide {
|
||||
width: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.navBar > * {
|
||||
max-width: 100%;
|
||||
color: white;
|
||||
}
|
||||
.header {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 3.5rem;
|
||||
top: 0;
|
||||
border: 1px solid #eef2f8;
|
||||
color: #4A4A4A;
|
||||
background-color: white;
|
||||
z-index: 20;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebarBtns {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebarBtns > li {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-left: 23px;
|
||||
}
|
||||
|
||||
.round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.bottomNavButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: .5rem;
|
||||
width: 100%;
|
||||
}
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 119 KiB |
@@ -8,7 +8,7 @@ self.addEventListener('install', function (event) {
|
||||
|
||||
var preLoad = function () {
|
||||
return caches.open('pwabuilder-offline').then(function (cache) {
|
||||
return cache.addAll(['/offline/', '/', '/404/', '/index.html']);
|
||||
return cache.addAll(['/offline/', '/', '/404.html', '/index.html']);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ var returnFromCache = function (request) {
|
||||
if (!matching) {
|
||||
return cache.match('/offline/')
|
||||
} else if (matching.status == 404) {
|
||||
return cache.match('/404/');
|
||||
return cache.match('/404.html');
|
||||
} else {
|
||||
return matching
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
//Add this below content to your HTML page, or add the js file to your page at the very top to register sercie worker
|
||||
if (navigator.serviceWorker.controller) {
|
||||
if (navigator && navigator.serviceWorker && navigator.serviceWorker.controller) {
|
||||
console.log('[PWA Builder] active service worker found, no need to register')
|
||||
} else {
|
||||
} else if (navigator && navigator.serviceWorker) {
|
||||
|
||||
//Register the ServiceWorker
|
||||
navigator.serviceWorker.register('/sw.min.js', {
|
||||
scope: './'
|
||||
}).then(function(reg) {
|
||||
console.log('Service worker has been registered for scope:'+ reg.scope);
|
||||
scope: '/'
|
||||
}).then(function (reg) {
|
||||
console.log('Service worker has been registered for scope:' + reg.scope);
|
||||
});
|
||||
}
|
||||
var fetched = [];
|
||||
if(fetch){
|
||||
document.querySelectorAll('a').forEach( a=> {
|
||||
if(a.href.includes('http') || a.href.includes('https') || a.href.includes('mailto') || a.href.includes('#') || fetched.includes(a.href)) {
|
||||
if (fetch) {
|
||||
document.querySelectorAll('a').forEach(a => {
|
||||
if (a.href.includes('http') || a.href.includes('https') || a.href.includes('mailto') || a.href.includes('#') || fetched.includes(a.href)) {
|
||||
return;
|
||||
}
|
||||
fetched.push(item.href);
|
||||
@@ -21,3 +21,46 @@ if(fetch){
|
||||
})
|
||||
}
|
||||
Promise.resolve(fetched);
|
||||
var triggerLazyImages = function () {
|
||||
document.querySelectorAll('.lazy').forEach(a => {
|
||||
var src = a.getAttribute('data-src');
|
||||
if (src) {
|
||||
a.src = src;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var toggleNav = function () {
|
||||
var nav = document.getElementById('navBar');
|
||||
if (!nav) {
|
||||
return;
|
||||
}
|
||||
var hidden = nav.classList.contains('hide');
|
||||
if (hidden) {
|
||||
nav.classList.remove('hide');
|
||||
triggerLazyImages();
|
||||
}
|
||||
else {
|
||||
nav.classList.add('hide');
|
||||
}
|
||||
}
|
||||
function attachNavToggle(elementId) {
|
||||
var menuButton = document.getElementById(elementId);
|
||||
if (menuButton) {
|
||||
menuButton.addEventListener('click', function () {
|
||||
toggleNav();
|
||||
});
|
||||
}
|
||||
}
|
||||
attachNavToggle('menuBtn');
|
||||
attachNavToggle('closeNav');
|
||||
document.addEventListener("readystatechange", function () {
|
||||
var nav = document.getElementById('navBar');
|
||||
if (!nav) {
|
||||
return;
|
||||
}
|
||||
var computedNav = window.getComputedStyle(nav);
|
||||
if (computedNav.width && computedNav.width !== "0px") {
|
||||
triggerLazyImages();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="32px" id="Layer_1" style="enable-background:new 0 0 32 32;" version="1.1" viewBox="0 0 32 32" width="32px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z M28,14H4c-1.104,0-2,0.896-2,2 s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2 S29.104,22,28,22z"/></svg>
|
||||
|
Before Width: | Height: | Size: 605 B |
1
src/TerribleDev.Blog.Web/wwwroot/svg/image.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M4 10h24c1.104 0 2-.896 2-2s-.896-2-2-2H4c-1.104 0-2 .896-2 2s.896 2 2 2zm24 4H4c-1.104 0-2 .896-2 2s.896 2 2 2h24c1.104 0 2-.896 2-2s-.896-2-2-2zm0 8H4c-1.104 0-2 .896-2 2s.896 2 2 2h24c1.104 0 2-.896 2-2s-.896-2-2-2z"/></svg>
|
||||
|
After Width: | Height: | Size: 299 B |