41 Commits

Author SHA1 Message Date
Tommy Parnell
728ef5b30d stop 2019-02-14 23:23:45 -05:00
tparnell
f51abe4339 stopping 2019-02-14 20:11:59 -05:00
Tommy Parnell
c3a583a33b add robots 2019-02-08 00:11:31 -05:00
Tommy Parnell
718b938a76 case insensitive tags 2019-02-07 23:20:44 -05:00
Tommy Parnell
415b62e1e7 get better 404 info 2019-02-07 23:13:13 -05:00
Tommy Parnell
e367072f21 alexa skills fix 2019-02-07 23:01:11 -05:00
tparnell
1327d87b96 site creator 2019-02-07 20:39:38 -05:00
tparnell
39dfcfe70d another twit meta 2019-02-07 20:37:16 -05:00
tparnell
86cb0cec23 more mettah fixes 2019-02-07 19:14:12 -05:00
tparnell
f87bd029f4 include redux gist 2019-02-07 18:47:05 -05:00
tparnell
69b2112e4b precconnect to analytics domain vs 2019-02-07 18:43:51 -05:00
tparnell
0b57031fc1 inject deps, fix links for rss feeds 2019-02-07 18:10:41 -05:00
tparnell
71929bb1ba fix external links 2019-02-07 16:06:52 -05:00
tparnell
b1959082dc I think I got links working 2019-02-06 22:38:30 -05:00
tparnell
be5c4cc806 fix rss 2019-02-06 22:32:14 -05:00
tparnell
ca0344c902 hopefully this fixes rss readers 2019-02-06 20:55:23 -05:00
tparnell
5a844f34f9 add img back in meta 2019-02-06 20:42:25 -05:00
tparnell
57a129cf8d tools for frontend devs 2019-02-06 19:12:18 -05:00
tparnell
a31b9d4fa9 stop 2019-02-06 19:01:46 -05:00
Tommy Parnell
7013e61c2f alt text for pictures 2019-02-06 07:56:24 -05:00
Tommy Parnell
6ce47adb8a webp avatar 2019-02-06 07:53:29 -05:00
tparnell
b9b9e81213 support webp 2019-02-05 23:32:21 -05:00
tparnell
53b8b448da webp 2019-02-05 23:32:21 -05:00
tparnell
aa6ed52d93 picture elem 2019-02-05 23:32:21 -05:00
Tommy Parnell
16c10c9ca1 app insights 2019-02-03 22:37:57 -05:00
Tommy Parnell
c3cb61619b fix some spacing 2019-02-03 21:40:49 -05:00
Tommy Parnell
7ff61450f9 Merge branch 'master' of github.com:TerribleDev/blog.terribledev.io.dotnet 2019-02-03 21:22:06 -05:00
Tommy Parnell
f3faede79e cache home page and posts in cf for 15 minutes 2019-02-03 21:21:57 -05:00
tparnell
38f82061e9 do not block anything 2019-02-03 20:22:21 -05:00
tparnell
57a8bba66a add csp back 2019-02-03 19:18:11 -05:00
tparnell
43d6e33638 rm csp 2019-02-03 19:11:28 -05:00
tparnell
d875ca6fea headers 🎉 2019-02-03 18:53:16 -05:00
Tommy Parnell
d846a538a0 headerz 2019-02-03 17:33:01 -05:00
Tommy Parnell
00b711aef4 increase hsts header 2019-02-03 13:14:20 -05:00
Tommy Parnell
dbb6ae208b output cache all the things 2019-02-03 13:01:21 -05:00
Tommy Parnell
de62e6275d inline styles 2019-02-03 11:53:52 -05:00
tparnell
d873be97d8 bettah config 2019-02-02 14:17:20 -05:00
tparnell
f3080faae0 start to move things to config 2019-02-02 13:49:46 -05:00
tparnell
6ed0ef4205 only render gtm in production 2019-02-02 13:30:30 -05:00
tparnell
ab9250b968 Revert "fix menu animation"
This reverts commit c24684fa8b.
2019-02-02 12:38:01 -05:00
tparnell
c24684fa8b fix menu animation 2019-02-02 12:28:55 -05:00
152 changed files with 1204 additions and 508 deletions

13
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"ban.spellright"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [
]
}

17
.vscode/launch.json vendored
View File

@@ -1,18 +1,17 @@
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/TerribleDev.Blog.Web/bin/Debug/netcoreapp2.2/TerribleDev.Blog.Web.dll",
"program": "${workspaceFolder}/src/TerribleDev.Blog.Web/bin/Debug/netcoreapp2.2/TerribleDev.Blog.Web.dll",
"args": [],
"cwd": "${workspaceFolder}/TerribleDev.Blog.Web",
"cwd": "${workspaceFolder}/src/TerribleDev.Blog.Web",
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart",
"launchBrowser": {
@@ -42,5 +41,5 @@
"request": "attach",
"processId": "${command:pickProcess}"
}
,]
]
}

19
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,19 @@
{
"files.eol": "\n",
"spellchecker.language": "en_US",
"spellchecker.ignoreWordsList": [
"dotnet",
"csproj's",
"VS2017",
"vs2017",
"refactor"
],
"spellchecker.documentTypes": [
"markdown",
"latex",
"plaintext"
],
"spellchecker.ignoreRegExp": [],
"spellchecker.ignoreFileExtensions": [],
"spellchecker.checkInterval": 5000
}

5
.vscode/spellright.dict vendored Normal file
View File

@@ -0,0 +1,5 @@
intellisense
docker
env
mydocklinting
eslint

2
.vscode/tasks.json vendored
View File

@@ -7,7 +7,7 @@
"type": "process",
"args": [
"build",
"${workspaceFolder}/TerribleDev.Blog.Web/TerribleDev.Blog.Web.csproj"
"${workspaceFolder}/src/TerribleDev.Blog.Web/TerribleDev.Blog.Web.csproj"
],
"problemMatcher": "$msCompile"
}

10
Readme.md Normal file
View File

@@ -0,0 +1,10 @@
## Compress webp
find . -iname '*.png' -exec cwebp -lossless '{}' -o '{}'.webp \;
find . -iname '*.jpg' -exec cwebp '{}' -o '{}'.webp \;
find . -iname '*.gif' -exec gif2webp -mixed '{}' -o '{}'.webp \;
## resize image
find . -iname '*' -exec convert '{}' -resize 750 '{}' \;

View File

@@ -1,34 +0,0 @@
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();
}
}
}

View File

@@ -1,62 +0,0 @@
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"), } });
}
}
}

View File

@@ -1,48 +0,0 @@
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;
}
}
}

View File

@@ -1,10 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.15.7" />
</ItemGroup>
</Project>

View File

@@ -7,10 +7,6 @@ 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
@@ -33,38 +29,12 @@ 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}

7
docker-compose.yml Normal file
View File

@@ -0,0 +1,7 @@
version: '3'
services:
webapp:
build: ./src/TerribleDev.Blog.Web
ports:
- "80:80"
- "443:443"

View File

@@ -1,85 +0,0 @@
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

View File

@@ -1,17 +0,0 @@
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>
}

View File

@@ -1,18 +0,0 @@
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
}

View File

@@ -1,9 +0,0 @@
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)

View File

@@ -1,23 +0,0 @@
<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>

View File

@@ -1,35 +0,0 @@
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
}

View File

@@ -1,8 +0,0 @@
module Constants
module Constants=
let MORE = "<!-- more -->"
let YMLDIVIDER = "---"
type Tokens =
| MORE
| YMLDIVIDER

View File

@@ -1,10 +0,0 @@
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)
}

View File

@@ -0,0 +1,7 @@
{
"ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider",
"Version": "8.14.11009.1",
"GettingStartedDocument": {
"Uri": "https://go.microsoft.com/fwlink/?LinkID=798432"
}
}

View File

@@ -12,48 +12,25 @@ namespace TerribleDev.Blog.Web.Controllers
{
public class HomeController : Controller
{
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<TerribleDev.Blog.Core.Models.Post>>(),
(accum, item) => {
foreach(var tag in item.tags)
{
if(accum.TryGetValue(tag, out var list))
{
list.Add(item);
}
else
{
accum[tag] = new List<TerribleDev.Blog.Core.Models.Post>() { item };
}
}
return accum;
});
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<TerribleDev.Blog.Core.Models.Post>() { item };
return accum;
}
accum[highestPage].Add(item);
return accum;
});
private readonly PostCache postCache;
public HomeController(PostCache postCache)
{
this.postCache = postCache;
}
[Route("/")]
[Route("/index.html")]
[Route("/page/{pageNumber?}" )]
[OutputCache(Duration = 31536000, VaryByParam = "pageNumber")]
[ResponseCache(Duration = 900)]
public IActionResult Index(int pageNumber = 1)
{
if(!postsByPage.TryGetValue(pageNumber, out var result))
if(!postCache.PostsByPage.TryGetValue(pageNumber, out var result))
{
return Redirect("/404/");
return Redirect($"/404/?from=/page/{pageNumber}/");
}
return View(new HomeViewModel() { Posts = result, Page = pageNumber, HasNext = postsByPage.ContainsKey(pageNumber + 1), HasPrevious = postsByPage.ContainsKey(pageNumber - 1) });
return View(new HomeViewModel() { Posts = result, Page = pageNumber, HasNext = postCache.PostsByPage.ContainsKey(pageNumber + 1), HasPrevious = postCache.PostsByPage.ContainsKey(pageNumber - 1) });
}
[Route("/theme/{postName?}")]
public IActionResult Theme(string postName)
@@ -76,12 +53,12 @@ namespace TerribleDev.Blog.Web.Controllers
[Route("{postUrl}")]
[OutputCache(Duration = 31536000, VaryByParam = "postUrl")]
[ResponseCache(Duration = 180)]
[ResponseCache(Duration = 900)]
public IActionResult Post(string postUrl)
{
if(!posts.TryGetValue(postUrl, out var currentPost))
if(!postCache.UrlToPost.TryGetValue(postUrl, out var currentPost))
{
return Redirect("/404/");
return Redirect($"/404/?from={postUrl}");
}
return View(model: currentPost);
}
@@ -95,10 +72,10 @@ namespace TerribleDev.Blog.Web.Controllers
[Route("/404")]
[Route("{*url}", Order = 999)]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult FourOhFour()
public IActionResult FourOhFour(string from = null)
{
this.Response.StatusCode = 404;
return View();
return View(viewName: nameof(FourOhFour));
}
[Route("/404.html")]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]

View File

@@ -15,12 +15,19 @@ namespace TerribleDev.Blog.Web.Controllers
{
public class SeoController : Controller
{
private readonly BlogConfiguration configuration;
private readonly PostCache postCache;
public SeoController(BlogConfiguration configuration, PostCache postCache)
{
this.configuration = configuration;
this.postCache = postCache;
}
public static DateTimeOffset publishDate = DateTimeOffset.UtcNow; // keep publish date in memory so we just return when the server was kicked
public static IEnumerable<SyndicationItem> postsToSyndication = HomeController.postsAsList.Select(Core.Models.Util.ToSyndicationItem).ToList();
[Route("/rss")]
[Route("/rss.xml")]
[ResponseCache(Duration = 7200)]
[OutputCache(Duration = 86400)]
[OutputCache(Duration = 31536000)]
public async Task Rss()
{
Response.StatusCode = 200;
@@ -28,11 +35,11 @@ namespace TerribleDev.Blog.Web.Controllers
using (XmlWriter xmlWriter = XmlWriter.Create(this.Response.Body, new XmlWriterSettings() { Async = true, Indent = false, Encoding = Encoding.UTF8 }))
{
var writer = new RssFeedWriter(xmlWriter);
await writer.WriteTitle("The Ramblings of TerribleDev");
await writer.WriteValue("link", "https://blog.terribledev.io");
await writer.WriteTitle(configuration.Title);
await writer.WriteValue("link", configuration.Link);
await writer.WriteDescription("My name is Tommy Parnell. I usually go by TerribleDev on the internets. These are just some of my writings and rants about the software space.");
foreach (var item in postsToSyndication)
foreach (var item in postCache.PostsAsSyndication)
{
await writer.Write(item);
}
@@ -43,19 +50,19 @@ namespace TerribleDev.Blog.Web.Controllers
}
[Route("/sitemap.xml")]
[ResponseCache(Duration = 7200)]
[OutputCache(Duration = 86400)]
[OutputCache(Duration = 31536000)]
public void SiteMap()
{
Response.StatusCode = 200;
Response.ContentType = "text/xml";
var sitewideLinks = new List<SiteMapItem>(HomeController.tagToPost.Keys.Select(a=> new SiteMapItem() { LastModified = DateTime.UtcNow, Location = $"https://blog.terribledev.io/tag/{a}/"}))
var sitewideLinks = new List<SiteMapItem>(postCache.TagsToPosts.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 = postCache.PostsAsLists.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);

View File

@@ -3,24 +3,31 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TerribleDev.Blog.Web.Models;
namespace TerribleDev.Blog.Web.Controllers
{
public class TagsController : Controller
{
private readonly PostCache postCache;
public TagsController(PostCache postCache)
{
this.postCache = postCache;
}
[Route("/all-tags")]
[OutputCache(Duration = 31536000)]
public IActionResult AllTags()
{
return View(HomeController.tagToPost);
return View(postCache.TagsToPosts);
}
[Route("/tag/{tagName}")]
[OutputCache(Duration = 31536000, VaryByParam = "tagName")]
public IActionResult GetTag(string tagName)
{
if(!HomeController.tagToPost.TryGetValue(tagName, out var models))
if(!postCache.TagsToPosts.TryGetValue(tagName.ToLower(), out var models))
{
return NotFound();
return Redirect($"/404/?from=/tag/{tagName}/");
}
{
return View(new Models.GetTagViewModel { Tag = tagName, Posts = models });

View File

@@ -0,0 +1,26 @@
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)
{
Uri.TryCreate($"https://blog.terribledev.io/{x.Url}/", UriKind.Absolute, out var url);
var syn = new SyndicationItem()
{
Title = x.Title,
Description = x.Content.ToString(),
Id = url.ToString(),
Published = x.PublishDate
};
syn.AddLink(new SyndicationLink(url));
return syn;
}
}
}

View File

@@ -0,0 +1,24 @@
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();
}
}
}

View File

@@ -0,0 +1,60 @@
using System.Collections.Generic;
using TerribleDev.Blog.Web.Models;
using System.Linq;
using System.Collections.Immutable;
using System.Diagnostics;
namespace TerribleDev.Blog.Web.Factories
{
public static class BlogCacheFactory
{
public static PostCache ProjectPostCache(IEnumerable<IPost> rawPosts)
{
var posts = rawPosts.OrderByDescending(a => a.PublishDate).ToImmutableList();
var tagsToPost = posts.Where(a=>a.tags != null)
.Aggregate(
ImmutableDictionary.Create<string, ImmutableList<IPost>>(),
(accum, item) => {
foreach(var tag in item.tags.Select(i => i.ToLower()))
{
if(accum.TryGetValue(tag, out var list))
{
accum = accum.SetItem(tag, list.Add(item));
}
else
{
accum = accum.Add(tag, ImmutableList.Create<IPost>(item));
}
}
return accum;
}).ToImmutableSortedDictionary();
var urlToPosts = posts.ToImmutableDictionary(a => a.Url);
var postsByPage =
posts.Aggregate(ImmutableDictionary.Create<int, ImmutableList<IPost>>(), (accum, item) =>
{
if(!accum.Keys.Any())
{
accum = accum.SetItem(1, ImmutableList.Create<IPost>());
}
var highestPage = accum.Keys.Any() ? accum.Keys.Max() : 1;
var current = accum[highestPage];
if (current.Count >= 10)
{
return accum.Add(highestPage + 1, ImmutableList.Create(item));
}
return accum.SetItem(highestPage, current.Add(item));
}).ToImmutableDictionary();
var syndicationPosts = posts.Select(i => i.ToSyndicationItem()).ToImmutableList();
return new PostCache()
{
PostsAsLists = posts,
TagsToPosts = tagsToPost,
UrlToPost = urlToPosts,
PostsByPage = postsByPage,
PostsAsSyndication = syndicationPosts
};
}
}
}

View File

@@ -0,0 +1,86 @@
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;
using TerribleDev.Blog.Web.MarkExtension.TerribleDev.Blog.Web.ExternalLinkParser;
using TerribleDev.Blog.Web.MarkExtension;
using Microsoft.AspNetCore.Hosting;
using System.Diagnostics;
using TerribleDev.Blog.Web.Factories;
namespace TerribleDev.Blog.Web
{
public class BlogFactory
{
private HighlightFactory highlightFactory = new HighlightFactory();
public List<IPost> GetAllPosts(string domain)
{
// why didn't I use f# I'd have a pipe operator by now
var posts = GetPosts();
var postsAsText = posts.Select(GetFileText);
return Task.WhenAll(postsAsText).Result.Select(b => ParsePost(b.text, b.fileInfo.Name, domain)).ToList();
}
private static async Task<(string text, FileInfo fileInfo)> GetFileText(string filePath)
{
var fileInfo = new FileInfo(filePath);
var text = await File.ReadAllTextAsync(fileInfo.FullName);
return (text, fileInfo);
}
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, string domain)
{
var splitFile = postText.Split("---");
var ymlRaw = splitFile[0];
var markdownText = string.Join("", splitFile.Skip(1));
var postSettings = ParseYaml(ymlRaw);
var resolvedUrl = !string.IsNullOrWhiteSpace(postSettings.permalink) ? postSettings.permalink : fileName.Split('.')[0].Replace(' ', '-').WithoutSpecialCharacters();
var codeBlocks = new List<string>();
List<string> postImages = new List<string>();
var pipeline = new MarkdownPipelineBuilder()
.Use(new AbsoluteLinkConverter(resolvedUrl, domain))
.Use<ImageRecorder>(new ImageRecorder(ref postImages))
.Use<TargetLinkExtension>()
.Use<PictureInline>()
.Use(new CodeRecorder(ref codeBlocks))
.UseMediaLinks()
.UseEmojiAndSmiley()
.Build();
var postContent = Markdown.ToHtml(markdownText, pipeline);
var postContentHighlighted = highlightFactory.Highlight(postContent);
var postContentPlain = String.Join("", Markdown.ToPlainText(markdownText, pipeline).Split("<!-- more -->"));
var summary = postContent.Split("<!-- more -->")[0];
var postSummaryPlain = postContentPlain.Split("<!-- more -->")[0];
return new Post()
{
PublishDate = postSettings.date.ToUniversalTime(),
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,
SummaryPlainShort = (postContentPlain.Length <= 147 ? postContentPlain : postContentPlain.Substring(0, 146)) + "...",
ContentPlain = postContentPlain,
Images = postImages.Distinct().ToList(),
CodeBlockLangs = codeBlocks
};
}
}
}

View File

@@ -0,0 +1,15 @@
using System.Text.RegularExpressions;
namespace TerribleDev.Blog.Web.Factories
{
public class HighlightFactory
{
private Regex codeFenceLang = new Regex("(?=<code class=\"language-(.*?)\">(.*?)(?=</code>))", RegexOptions.Compiled | RegexOptions.Singleline);
public string Highlight(string input)
{
return codeFenceLang.Replace(input, m => {
return m.ToString();
});
}
}
}

View File

@@ -0,0 +1,14 @@
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);
}
}

View File

@@ -0,0 +1,15 @@
using System.Text.RegularExpressions;
namespace TerribleDev.Blog.Web.Factories.Processors
{
public class JavaScriptProcessor
{
private Regex keywordRegex = new Regex(@"\b(in|of|if|for|while|finally|var|new|function|do|return|void|else|break|catch|instanceof|with|throw|case|default|try|this|switch|continue|typeof|delete|let|yield|const|export|super|debugger|as|async|await|static|import|from|as|)(?=[^\w])", RegexOptions.Compiled | RegexOptions.Multiline);
private Regex literalRegex = new Regex(@"\b(true|false|null|undefined|NaN|Infinity)(?=[^\w])", RegexOptions.Compiled | RegexOptions.Multiline);
private Regex[] quoteMarkRegexes = new Regex[]{ new Regex("(.*?)", RegexOptions.Compiled | RegexOptions.Multiline), new Regex(@"'(.*?)'", RegexOptions.Compiled | RegexOptions.Multiline)};
public string Process(string input)
{
return input;
}
}
}

View File

@@ -0,0 +1,61 @@
using System;
using Markdig;
using Markdig.Renderers;
using Markdig.Renderers.Html.Inlines;
using Markdig.Syntax.Inlines;
namespace TerribleDev.Blog.Web.MarkExtension
{
public class AbsoluteLinkConverter : IMarkdownExtension
{
public string BaseUrl { get; }
public string Domain { get; }
public AbsoluteLinkConverter(string baseUrl, string domain)
{
BaseUrl = baseUrl;
Domain = domain;
}
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
var htmlRenderer = renderer as HtmlRenderer;
if (htmlRenderer != null)
{
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();
inlineRenderer.TryWriters.Add(TryLinkAbsoluteUrlWriter);
}
}
private bool TryLinkAbsoluteUrlWriter(HtmlRenderer renderer, LinkInline linkInline)
{
var prevDynamic = linkInline.GetDynamicUrl;
linkInline.GetDynamicUrl = () => {
var escapeUrl = prevDynamic != null ? prevDynamic() ?? linkInline.Url : linkInline.Url;
if(!System.Uri.TryCreate(escapeUrl, UriKind.RelativeOrAbsolute, out var parsedResult))
{
throw new Exception($"Error making link for {escapeUrl} @ {BaseUrl}");
}
if(parsedResult.IsAbsoluteUri)
{
return escapeUrl;
}
var uriBuilder = new UriBuilder(Domain);
if(!escapeUrl.StartsWith("/"))
{
uriBuilder = uriBuilder.WithPathSegment($"/{BaseUrl}/{escapeUrl}");
}
else
{
uriBuilder = uriBuilder.WithPathSegment(parsedResult.ToString());
}
return uriBuilder.Uri.ToString();
};
return false;
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using Markdig;
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
namespace TerribleDev.Blog.Web.MarkExtension
{
public class CodeRecorder : IMarkdownExtension
{
public CodeRecorder(ref List<string> codeLanguages)
{
CodeLanguages = codeLanguages;
}
public List<string> CodeLanguages { get; }
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
var htmlRenderer = renderer as HtmlRenderer;
if (htmlRenderer != null)
{
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<CodeBlockRenderer>();
if (inlineRenderer != null)
{
inlineRenderer.TryWriters.Add(TryWriter);
}
}
}
private bool TryWriter(HtmlRenderer renderer, CodeBlock block)
{
var fencedBlock = block as FencedCodeBlock;
if(fencedBlock == null || fencedBlock.Info == null)
{
return false;
}
CodeLanguages.Add(fencedBlock.Info ?? "");
return false;
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Markdig.Syntax.Inlines;
namespace TerribleDev.Blog.Web.MarkExtension
{
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;
namespace TerribleDev.Blog.Web.ExternalLinkParser
{
/// <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"), } });
}
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Markdig.Syntax.Inlines;
namespace TerribleDev.Blog.Web.MarkExtension
{
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;
namespace TerribleDev.Blog.Web.ExternalLinkParser
{
public class ImageRecorder : IMarkdownExtension
{
private List<string> images = null;
public ImageRecorder(ref 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.Add(TryLinkInlineRenderer);
}
}
}
private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline)
{
if (linkInline.Url == null || !linkInline.IsImage)
{
return false;
}
var url = linkInline.GetDynamicUrl != null ? linkInline.GetDynamicUrl(): linkInline.Url;
this.images.Add(url);
return false;
}
}
}
}

View File

@@ -0,0 +1,69 @@
using System;
using Markdig;
using Markdig.Renderers;
using Markdig.Renderers.Html.Inlines;
using Markdig.Syntax.Inlines;
namespace TerribleDev.Blog.Web.MarkExtension
{
public class PictureInline : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
var htmlRenderer = renderer as HtmlRenderer;
if (htmlRenderer != null)
{
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();
inlineRenderer.TryWriters.Add(TryLinkInlineRenderer);
}
}
private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline)
{
if (linkInline == null || !linkInline.IsImage)
{
return false;
}
renderer.Write("<picture>");
WriteImageTag(renderer, linkInline, ".webp", "image/webp");
WriteImageTag(renderer, linkInline, string.Empty);
renderer.Write("</picture>");
return true;
}
private void WriteImageTag(HtmlRenderer renderer, LinkInline link, string suffix, string type = null)
{
renderer.Write(string.IsNullOrWhiteSpace(type) ? $"<img src=\"" : $"<source type=\"{type}\" srcset=\"");
var escapeUrl = link.GetDynamicUrl != null ? link.GetDynamicUrl() ?? link.Url : link.Url;
renderer.WriteEscapeUrl($"{escapeUrl}{suffix}");
renderer.Write("\"");
renderer.WriteAttributes(link);
if (renderer.EnableHtmlForInline)
{
renderer.Write(" alt=\"");
}
var wasEnableHtmlForInline = renderer.EnableHtmlForInline;
renderer.EnableHtmlForInline = false;
renderer.WriteChildren(link);
renderer.EnableHtmlForInline = wasEnableHtmlForInline;
if (renderer.EnableHtmlForInline)
{
renderer.Write("\"");
}
if (renderer.EnableHtmlForInline)
{
renderer.Write(" />");
}
}
}
}

View File

@@ -0,0 +1,8 @@
namespace TerribleDev.Blog.Web.Models
{
public class BlogConfiguration
{
public string Title { get; set; }
public string Link { get; set; }
}
}

View File

@@ -7,7 +7,7 @@ namespace TerribleDev.Blog.Web.Models
{
public class GetTagViewModel
{
public IEnumerable<TerribleDev.Blog.Core.Models.Post> Posts { get; set; }
public IEnumerable<IPost> Posts { get; set; }
public string Tag { get; set; }
}
}

View File

@@ -4,7 +4,7 @@ namespace TerribleDev.Blog.Web.Models
{
public class HomeViewModel
{
public IEnumerable<TerribleDev.Blog.Core.Models.Post> Posts { get; set;}
public IEnumerable<IPost> Posts { get; set;}
public int Page { get; set; }
public string NextUrl { get; set; }
public string PreviousUrl { get; set; }

View File

@@ -0,0 +1,24 @@
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; }
string SummaryPlainShort { get; set; }
IList<string> tags { get; set; }
IList<string> Images { get; set;}
IList<string> CodeBlockLangs { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
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; }
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Html;
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace TerribleDev.Blog.Web.Models
{
[DebuggerDisplay("{Title}")]
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 string SummaryPlainShort { get; set; }
public IList<string> tags { get; set; }
public IList<string> Images { get; set;}
public IList<string> CodeBlockLangs { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Immutable;
using Microsoft.SyndicationFeed;
namespace TerribleDev.Blog.Web.Models
{
public class PostCache
{
public ImmutableList<IPost> PostsAsLists { get; set;}
public ImmutableSortedDictionary<string, ImmutableList<IPost>> TagsToPosts { get; set; }
public ImmutableDictionary<string, IPost> UrlToPost { get; set; }
public ImmutableDictionary<int, ImmutableList<IPost>> PostsByPage { get; set; }
public ImmutableList<SyndicationItem> PostsAsSyndication { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Html;
namespace TerribleDev.Blog.Web.Models
{
public class PostModel
{
public HtmlString Content { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
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; }
}
}

View File

@@ -15,7 +15,7 @@ Getting Started:
Ok, so the alexa .net sdk is for the full framework only, and its built for webapi. The best way to get going is in visual studio `file -> new project -> ASP.NET Web Application .net framework` A dialog comes up, and I picked `Azure API App`.
![dialog picker](dialog.png)
![dialog picker](dialog.PNG)
Now you have an empty webapi project. We don't need swashbuckle/swagger so lets get rid of that

View File

@@ -0,0 +1,49 @@
title: Must have vscode plugins for front-end devs
date: 2019-02-06
tags:
- visual studio
- javascript
- css
- front-end
---
I've had a lot of people ask me about my choice of editors, and plugins. A while back I switched to vscode for all my programming work, for both front and back end. In the past I've blogged about [the best plugins for visual studio](/VS-2017-best-extensions-on-launch/) as a backend dev, but I thought I'd give you a more front-end angle
<!-- more -->
## Document this
My first one, and in my opinion the most underrated is [document this](https://marketplace.visualstudio.com/items?itemName=joelday.docthis). So if you have ever had to write [jsdoc](http://usejsdoc.org/) comments you can know how tedious it gets, and if you haven't, trust me you should. VSCode and most other editors can read [jsdoc](http://usejsdoc.org/) comments above functions, and class declarations to improve the intellisense and type completion statements. Simply have your cursor over a function, invoke document this, and quickly you will be given jsdoc comments for your code.
![Animated gif showing off document this](document-this.gif)
## Import Cost
Another extension I find vital to my every day is [import cost](https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost). This is a package, that leaves you little notes on the side of any import you have as to how big it will be. This package will even highlight the size text in red for large imports which you can configure. What I love about this package, is it tells me if the package I'm about to use is going to be very expensive size wise. That way I find out long before I commit the code, and my pages get slow.
![a static image showing off import cost](import-cost.png)
## ESlint and Prettier
Hopefully both of these will not be new to you. ESLint is a linting tool that looks for potential errors in your code. Prettier is an opinionated style enforcer for your code. The [eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) extensions for vscode can automatically show you problems in your code as you type, and can even fix your code on save. What I love about both of these tools, is together they make a great force for improving your code base. Prettier eliminates many debates over code style between team members, and eslint prevents you from shipping many bugs to production. These extensions can call out problems as you type, which decreases the feedback loops, and increases your productivity.
## Filesize
As a web developer I spend a lot of my time looking at file size. Right now file sizes are ever inflating, and are causing pain for bandwidth constrained devices. I often download bundles, and inspect their compiled source, or just have to look at how big a file is on the filesystem. A big tool I have in my belt is [filesize](https://marketplace.visualstudio.com/items?itemName=mkxml.vscode-filesize). This is a crazy simple extension, but one that brings me joy everyday. The premise is simple, print the file size of the current file in the status bar at the bottom. Click on it, and you get a nice output of what its like gzipped, and the mime type. Dirt simple, but saved me a ton of time everyday!
![a picture of the filesize plugin in action](filesize2.jpg)
## Runner ups
Here is a list of additional extensions I certainly couldn't live without
* [path intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense) - autocomplete file paths in various files (including html)
* [npm intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense) - autocomplete npm pages in imports
* [html 5 boilerplate](https://marketplace.visualstudio.com/items?itemName=sidthesloth.html5-boilerplate) - dirt simple html boilerplate snippets
* [icon fonts](https://marketplace.visualstudio.com/items?itemName=idleberg.icon-fonts) - Autocomplete for various icon fonts such as font awesome
* [git lens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) - Show git history inline, along with other information from git

View File

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

View File

@@ -19,6 +19,7 @@ namespace TerribleDev.Blog.Web
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseApplicationInsights()
.UseStartup<Startup>()
.ConfigureKestrel(a =>
{

View File

@@ -15,20 +15,38 @@ using Microsoft.Extensions.FileProviders;
using Microsoft.Net.Http.Headers;
using HardHat.Middlewares;
using HardHat;
using TerribleDev.Blog.Web.Models;
using TerribleDev.Blog.Web.Factories;
namespace TerribleDev.Blog.Web
{
public class Startup
{
public Startup(IConfiguration configuration)
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
Configuration = configuration;
Env = env;
}
public IConfiguration Configuration { get; }
public IHostingEnvironment Env { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
Func<BlogConfiguration> getBlog = () => Configuration.GetSection("Blog").Get<BlogConfiguration>();
if (Env.IsDevelopment())
{
services.AddTransient(a => getBlog());
}
else
{
services.AddSingleton(getBlog());
}
services.AddSingleton(i => {
var posts = new BlogFactory().GetAllPosts(Env.IsDevelopment() ? "https://localhost:5001": "https://blog.terribledev.io");
return BlogCacheFactory.ProjectPostCache(posts);
});
services.AddResponseCompression(a =>
{
a.EnableForHttps = true;
@@ -57,31 +75,47 @@ namespace TerribleDev.Blog.Web
app.UseHttpsRedirection();
app.UseResponseCompression();
var cacheTime = env.IsDevelopment() ? 0 : 31536000;
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
"public,max-age=" + cacheTime;
}
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
"public,max-age=" + cacheTime;
}
});
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "img")),
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
"public,max-age=" + cacheTime;
}
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
"public,max-age=" + cacheTime;
}
});
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.UseHsts(TimeSpan.FromDays(365), false, preload: true);
app.UseContentSecurityPolicy(
new ContentSecurityPolicy()
{
// DefaultSrc = new HashSet<string>() {
// CSPConstants.Self, "https://www.google-analytics.com", "https://www.googletagmanager.com", "https://stats.g.doubleclick.net"
// },
// ScriptSrc = new HashSet<string>()
// {
// CSPConstants.Self, CSPConstants.UnsafeInline, "https://www.google-analytics.com", "https://www.googletagmanager.com", "https://stats.g.doubleclick.net"
// },
// StyleSrc = new HashSet<string>()
// {
// CSPConstants.Self, CSPConstants.UnsafeInline
// },
UpgradeInsecureRequests = true
});
app.UseOutputCaching();
app.UseMvc();
}

View File

@@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
using System.IO;
using System.Threading.Tasks;
namespace TerribleDev.Blog.Web.Taghelpers
{
[HtmlTargetElement("inline-style")]
public class InlineStyleTagHelper : TagHelper
{
[HtmlAttributeName("href")]
public string Href { get; set; }
private IHostingEnvironment HostingEnvironment { get; }
private IMemoryCache Cache { get; }
public InlineStyleTagHelper(IHostingEnvironment hostingEnvironment, IMemoryCache cache)
{
HostingEnvironment = hostingEnvironment;
Cache = cache;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var path = Href;
// Get the value from the cache, or compute the value and add it to the cache
var fileContent = await Cache.GetOrCreateAsync("InlineStyleTagHelper-" + path, async entry =>
{
var fileProvider = HostingEnvironment.WebRootFileProvider;
if(HostingEnvironment.IsDevelopment())
{
var changeToken = fileProvider.Watch(path);
entry.AddExpirationToken(changeToken);
}
entry.SetPriority(CacheItemPriority.NeverRemove);
var file = fileProvider.GetFileInfo(path);
if (file == null || !file.Exists)
return null;
return await ReadFileContent(file);
});
if (fileContent == null)
{
output.SuppressOutput();
return;
}
output.TagName = "style";
output.Attributes.RemoveAll("href");
output.Content.AppendHtml(fileContent);
}
private static async Task<string> ReadFileContent(IFileInfo file)
{
using (var stream = file.CreateReadStream())
using (var textReader = new StreamReader(stream))
{
return await textReader.ReadToEndAsync();
}
}
}
}

View File

@@ -0,0 +1,41 @@
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();
}
}
}
}

View File

@@ -5,6 +5,8 @@
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<UserSecretsId>9a1f51b6-f4d9-4df7-a0af-e345176e9927</UserSecretsId>
<ApplicationInsightsResourceId>/subscriptions/088a81c7-d703-41c9-a1d0-476bce11df60/resourcegroups/WebResourceGroup/providers/microsoft.insights/components/tparnellblognew</ApplicationInsightsResourceId>
<ApplicationInsightsAnnotationResourceId>/subscriptions/088a81c7-d703-41c9-a1d0-476bce11df60/resourcegroups/WebResourceGroup/providers/microsoft.insights/components/tparnellblognew</ApplicationInsightsAnnotationResourceId>
</PropertyGroup>
<ItemGroup>
@@ -21,12 +23,14 @@
<ItemGroup>
<PackageReference Include="BuildBundlerMinifier" Version="2.8.391" />
<PackageReference Include="Markdig" Version="0.15.7" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.0.2105168" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.0" />
<PackageReference Include="UriBuilder.Fluent" Version="1.5.2" />
<PackageReference Include="YamlDotNet" Version="5.3.0" />
<PackageReference Include="HardHat" Version="2.0.0" />
<PackageReference Include="HardHat" Version="2.1.1" />
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="WebEssentials.AspNetCore.OutputCaching" Version="1.0.16" />
</ItemGroup>
@@ -36,12 +40,9 @@
<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" />
<WCFMetadata Include="Connected Services" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,5 @@
@model TerribleDev.Blog.Core.Models.Post
@inject BlogConfiguration config
@model IPost
@{
ViewData["Title"] = "Post";
ViewData["HideNav"] = true;
@@ -13,20 +14,30 @@
<meta property="og:type" content="blog">
<meta property="og:title" content="@Model.Title">
<meta property="og:url" content="https://blog.terribledev.io/@Model.Url/">
<meta property="og:site_name" content="The Ramblings of TerribleDev">
<meta property="og:site_name" content="@config.Title">
<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.SummaryPlainShort">
<meta name="twitter:site" content="@@TerribleDev">
<meta name="twitter:creator" content="@@TerribleDev">
<meta property="og:image" content="https://www.gravatar.com/avatar/333e3cea32cd17ff2007d131df336061?s=640" />
@foreach(var image in Model.Images.Take(6))
{
<meta property="og:image" content="https://blog.terribledev.io@(image)">
<meta property="og:image" content="@image">
}
@if(Model.Images.Length > 0)
@if(Model.Images.Count > 0)
{
<meta name="twitter:image" content="@(Model.Images[0])">
}
<meta property="og:image" content="https://www.gravatar.com/avatar/333e3cea32cd17ff2007d131df336061?s=640" />
@if(Model.CodeBlockLangs.Count > 0)
{
<meta name="twitter:image" content="https://blog.terribledev.io@(Model.Images[0])">
}
}
@section Scripts
{
@if(Model.CodeBlockLangs.Count > 0)
{
}
}

View File

@@ -1,10 +1,11 @@
@model TerribleDev.Blog.Core.Models.Post
@using Newtonsoft.Json
@model IPost
<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.Length > 0)
@if (Model.tags.Count > 0)
{
<div>
<span>Tagged In:</span><br />
@@ -15,4 +16,12 @@
</div>
}
<environment names="Development">
<pre>
<code>
@JsonConvert.SerializeObject(Model, Formatting.Indented)
</code>
</pre>
</environment>
</article>

View File

@@ -1,15 +1,18 @@
@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet
@{
Layout = null;
}
<link rel="preconnect" href="https://www.google-analytics.com">
<link rel="preconnect" href="https://stats.g.doubleclick.net">
<link rel="preconnect" href="https://www.googletagmanager.com">
<link rel="preconnect" href="https://az416426.vo.msecnd.net" />
<link rel="preconnect" href="https://dc.services.visualstudio.com" />
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-48128396-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-48128396-1');
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'UA-48128396-1');
</script>
@Html.Raw(JavaScriptSnippet.FullScript)

View File

@@ -2,7 +2,10 @@
var hideNav = ViewData["HideNav"] != null ? "" : "withBody";
}
<nav class="navBar hide @hideNav" id="navBar">
<img src="" alt="An image of TerribleDev" data-src="/content/tommyAvatar4.jpg" class="lazy round" />
<picture>
<source srcset="" type="image/webp" alt="An image of TerribleDev" data-src="/content/tommyAvatar4.jpg.webp" class="lazy round" />
<img src="" alt="An image of TerribleDev" data-src="/content/tommyAvatar4.jpg" class="lazy round" />
</picture>
<span>Tommy "Terrible Dev" Parnell</span>
<ul class="sidebarBtns">
<li><a href="/" class="link-unstyled">Home</a></li>

View File

@@ -1,4 +1,4 @@
@model TerribleDev.Blog.Core.Models.Post
@model IPost
<article class="btmRule">
<h3 itemprop="headline" class="headline"><a href="/@Model.Url/" class="link-unstyled">@Model.Title</a></h3>

View File

@@ -1,11 +1,12 @@
<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." />
@inject BlogConfiguration config
<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:title" content="@config.Title">
<meta property="og:url" content="https://blog.terribledev.io/">
<meta property="og:site_name" content="The Ramblings of TerribleDev">
<meta property="og:site_name" content="@config.Title">
<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">
<meta name="twitter:title" content="The Ramblings of TerribleDev">
<meta name="twitter:title" content="@config.Title">
<meta name="twitter: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:creator" content="@@TerribleDev">
<meta property="og:image" content="https://www.gravatar.com/avatar/333e3cea32cd17ff2007d131df336061?s=640" />

View File

@@ -1,20 +1,24 @@
<!DOCTYPE html>
@inject BlogConfiguration config
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<partial name="Gtm" />
<environment names="Production">
<partial name="Gtm" />
</environment>
<meta name="author" content="Tommy &quot;TerribleDev&quot; Parnell" />
<meta name="theme-color" content="#4A4A4A" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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>
<title>@ViewData["Title"] - @config.Title</title>
<environment names="Development">
<link asp-append-version="true" rel="stylesheet" href="~/css/site.css" />
<inline-style href="css/site.css"></inline-style>
</environment>
<environment names="Production">
<link asp-append-version="true" rel="stylesheet" href="~/css/site.min.css" />
<inline-style href="css/site.min.css"></inline-style>
</environment>
@RenderSection("Head", false)
</head>
@@ -22,7 +26,7 @@
<partial name="Nav" />
<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>
<div class="headerCallout"><a href="/" class="link-unstyled ">@config.Title</a></div>
</header>
@{
var bodyBump = ViewData["HideNav"] == null ? "bodyWithNav": "";
@@ -32,12 +36,12 @@
@RenderBody()
</div>
</main>
@RenderSection("Scripts", required: false)
<environment names="Development">
<script asp-append-version="true" src="~/js/swi.js" async></script>
</environment>
<environment names="Production">
<script asp-append-version="true" src="~/js/site.min.js" async></script>
</environment>
@RenderSection("Scripts", required: false)
</body>
</html>

View File

@@ -1,13 +1,14 @@
@model Dictionary<string, List<TerribleDev.Blog.Core.Models.Post>>
@using System.Collections.Immutable;
@model IDictionary<string, ImmutableList<IPost>>
@{
ViewData["Title"] = "all-tags";
}
<h2>All Tags</h2>
<cache>
@foreach (var tag in Model.Keys)
{
<a href="/tag/@tag/" class="btn block">@tag</a>
}
@foreach (var tag in Model.Keys)
{
<a href="/tag/@tag/" class="btn block">@tag</a>
}
</cache>
@section Head {
<partial name="StockMeta" />

View File

@@ -1,3 +1,4 @@
@using TerribleDev.Blog.Web
@using TerribleDev.Blog.Web.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, TerribleDev.Blog.Web

View File

@@ -0,0 +1,5 @@
{
"ApplicationInsights": {
"InstrumentationKey": "974b47d2-1f08-42df-b498-bbfda7425f0b"
}
}

View File

@@ -9,5 +9,9 @@
}
}
},
"AllowedHosts": "*"
}
"AllowedHosts": "*",
"Blog": {
"title": "The Ramblings of TerribleDev",
"link": "https://blog.terribledev.io"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Some files were not shown because too many files have changed in this diff Show More