Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17d69ac124 | ||
|
|
be5c4cc806 | ||
|
|
ca0344c902 | ||
|
|
5a844f34f9 | ||
|
|
57a129cf8d | ||
|
|
a31b9d4fa9 | ||
|
|
7013e61c2f | ||
|
|
6ce47adb8a | ||
|
|
b9b9e81213 | ||
|
|
53b8b448da | ||
|
|
aa6ed52d93 | ||
|
|
16c10c9ca1 | ||
|
|
c3cb61619b | ||
|
|
7ff61450f9 | ||
|
|
f3faede79e | ||
|
|
38f82061e9 | ||
|
|
57a8bba66a | ||
|
|
43d6e33638 | ||
|
|
d875ca6fea | ||
|
|
d846a538a0 | ||
|
|
00b711aef4 | ||
|
|
dbb6ae208b | ||
|
|
de62e6275d | ||
|
|
d873be97d8 | ||
|
|
f3080faae0 | ||
|
|
6ed0ef4205 | ||
|
|
ab9250b968 | ||
|
|
c24684fa8b |
13
.vscode/extensions.json
vendored
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
|
||||
intellisense
|
||||
docker
|
||||
env
|
||||
mydocklinting
|
||||
eslint
|
||||
2
.vscode/tasks.json
vendored
@@ -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
@@ -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 '{}' \;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"), } });
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
@@ -0,0 +1,7 @@
|
||||
version: '3'
|
||||
services:
|
||||
webapp:
|
||||
build: ./src/TerribleDev.Blog.Web
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
module Constants
|
||||
module Constants=
|
||||
|
||||
let MORE = "<!-- more -->"
|
||||
let YMLDIVIDER = "---"
|
||||
type Tokens =
|
||||
| MORE
|
||||
| YMLDIVIDER
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider",
|
||||
"Version": "8.14.11009.1",
|
||||
"GettingStartedDocument": {
|
||||
"Uri": "https://go.microsoft.com/fwlink/?LinkID=798432"
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ 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)
|
||||
public static List<IPost> postsAsList = new BlogFactory().GetAllPosts().OrderByDescending(a=>a.PublishDate).ToList();
|
||||
public static Dictionary<string, List<IPost>> tagToPost = postsAsList.Where(a=>a.tags != null)
|
||||
.Aggregate(
|
||||
new Dictionary<string, List<TerribleDev.Blog.Core.Models.Post>>(),
|
||||
new Dictionary<string, List<IPost>>(),
|
||||
(accum, item) => {
|
||||
foreach(var tag in item.tags)
|
||||
{
|
||||
@@ -25,19 +25,19 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
accum[tag] = new List<TerribleDev.Blog.Core.Models.Post>() { item };
|
||||
accum[tag] = new List<IPost>() { 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) =>
|
||||
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) =>
|
||||
{
|
||||
var highestPage = accum.Keys.Max();
|
||||
var current = accum[highestPage].Count;
|
||||
if (current >= 10)
|
||||
{
|
||||
accum[highestPage + 1] = new List<TerribleDev.Blog.Core.Models.Post>() { item };
|
||||
accum[highestPage + 1] = new List<IPost>() { item };
|
||||
return accum;
|
||||
}
|
||||
accum[highestPage].Add(item);
|
||||
@@ -47,6 +47,8 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
[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))
|
||||
@@ -76,7 +78,7 @@ 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))
|
||||
|
||||
@@ -15,12 +15,18 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
{
|
||||
public class SeoController : Controller
|
||||
{
|
||||
private readonly BlogConfiguration configuration;
|
||||
public SeoController(BlogConfiguration configuration)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
|
||||
}
|
||||
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();
|
||||
public static IEnumerable<SyndicationItem> postsToSyndication = HomeController.postsAsList.Select(a => a.ToSyndicationItem()).ToList();
|
||||
[Route("/rss")]
|
||||
[Route("/rss.xml")]
|
||||
[ResponseCache(Duration = 7200)]
|
||||
[OutputCache(Duration = 86400)]
|
||||
[OutputCache(Duration = 31536000)]
|
||||
public async Task Rss()
|
||||
{
|
||||
Response.StatusCode = 200;
|
||||
@@ -28,8 +34,8 @@ 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)
|
||||
@@ -43,12 +49,12 @@ 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>(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/" }
|
||||
};
|
||||
|
||||
25
src/TerribleDev.Blog.Web/Extensions/IPostExtensions.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/TerribleDev.Blog.Web/Extensions/StringExtension.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/TerribleDev.Blog.Web/Factories/BlogFactory.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
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;
|
||||
|
||||
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 postsAsText = posts.Select(GetFileText);
|
||||
return Task.WhenAll(postsAsText).Result.AsParallel().Select(b => ParsePost(b.text, b.fileInfo.Name)).ToList();
|
||||
}
|
||||
|
||||
private static async Task<(string text, FileInfo fileInfo)> GetFileText(string filePath)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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();
|
||||
List<string> postImages = new List<string>();
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.Use<PictureInline>(new PictureInline(resolvedUrl))
|
||||
.Use<TargetLinkExtension>()
|
||||
.Use<ImageRecorder>(new ImageRecorder(ref postImages))
|
||||
.UseMediaLinks()
|
||||
.UseEmojiAndSmiley()
|
||||
.Build();
|
||||
var postContent = Markdown.ToHtml(markdownText, pipeline);
|
||||
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().Select(a => a.StartsWith('/') ? a : $"/{resolvedUrl}/{a}").ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/TerribleDev.Blog.Web/Factories/IBlogFactory.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
70
src/TerribleDev.Blog.Web/MarkExtension/ExternalLinkParser.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
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"), } });
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/TerribleDev.Blog.Web/MarkExtension/ImageRecorder.cs
Normal 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.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/TerribleDev.Blog.Web/MarkExtension/LinkConverter.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
|
||||
namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
public class LinkConverter : IMarkdownExtension
|
||||
{
|
||||
private readonly Func<string, string> convertLink;
|
||||
|
||||
public LinkConverter(Func<string, string> convertLink)
|
||||
{
|
||||
this.convertLink = convertLink;
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if(htmlRenderer == null) return;
|
||||
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();
|
||||
if(inlineRenderer == null) return;
|
||||
inlineRenderer.TryWriters.Add((ren, inline) => {
|
||||
return false;
|
||||
inline.GetDynamicUrl =
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/TerribleDev.Blog.Web/MarkExtension/PictureInline.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
|
||||
namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
public class PictureInline : IMarkdownExtension
|
||||
{
|
||||
private readonly string postUrl;
|
||||
public PictureInline(string postUrl)
|
||||
{
|
||||
this.postUrl = postUrl;
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null && !htmlRenderer.ObjectRenderers.Contains<PictureInlineRenderer>())
|
||||
{
|
||||
htmlRenderer.ObjectRenderers.ReplaceOrAdd<LinkInlineRenderer>(new PictureInlineRenderer(postUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
// <summary>
|
||||
/// A HTML renderer for a <see cref="LinkInline"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{Markdig.Syntax.Inlines.LinkInline}" />
|
||||
public class PictureInlineRenderer : LinkInlineRenderer
|
||||
{
|
||||
private readonly string baseUrl;
|
||||
public PictureInlineRenderer(string baseUrl)
|
||||
{
|
||||
this.baseUrl = baseUrl;
|
||||
|
||||
}
|
||||
private void WriteImageTag(HtmlRenderer renderer, LinkInline link, string suffix, string type = null)
|
||||
{
|
||||
renderer.Write(string.IsNullOrWhiteSpace(type) ? $"<img src=\"" : $"<source type=\"{type}\" srcset=\"");
|
||||
var escapeUrl = link.GetDynamicUrl != null ? link.GetDynamicUrl() ?? link.Url : link.Url;
|
||||
//todo: this should be a seperate plugin
|
||||
// urls that are like "3.png" should resolve to /<postUrl>/3.png mostly for rss readers
|
||||
if(!System.Uri.TryCreate(escapeUrl, UriKind.RelativeOrAbsolute, out var parsedResult))
|
||||
{
|
||||
throw new Exception($"Error making link for {escapeUrl} @ {baseUrl}");
|
||||
}
|
||||
if(!escapeUrl.StartsWith("/"))
|
||||
{
|
||||
escapeUrl = $"{baseUrl}/{escapeUrl}";
|
||||
}
|
||||
renderer.WriteEscapeUrl($"{escapeUrl}{suffix}");
|
||||
renderer.Write("\"");
|
||||
renderer.WriteAttributes(link);
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write(" alt=\"");
|
||||
}
|
||||
var wasEnableHtmlForInline = renderer.EnableHtmlForInline;
|
||||
renderer.EnableHtmlForInline = false;
|
||||
renderer.WriteChildren(link);
|
||||
renderer.EnableHtmlForInline = wasEnableHtmlForInline;
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write("\"");
|
||||
}
|
||||
|
||||
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write(" />");
|
||||
}
|
||||
}
|
||||
protected override void Write(HtmlRenderer renderer, LinkInline link)
|
||||
{
|
||||
if (!link.IsImage)
|
||||
{
|
||||
base.Write(renderer, link);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
renderer.Write("<picture>");
|
||||
WriteImageTag(renderer, link, ".webp", "image/webp");
|
||||
WriteImageTag(renderer, link, string.Empty);
|
||||
renderer.Write("</picture>");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/TerribleDev.Blog.Web/Models/BlogConfiguration.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class BlogConfiguration
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
23
src/TerribleDev.Blog.Web/Models/IPost.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
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;}
|
||||
}
|
||||
}
|
||||
16
src/TerribleDev.Blog.Web/Models/IPostSettings.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
22
src/TerribleDev.Blog.Web/Models/Post.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
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;}
|
||||
}
|
||||
}
|
||||
9
src/TerribleDev.Blog.Web/Models/PostModel.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class PostModel
|
||||
{
|
||||
public HtmlString Content { get; set; }
|
||||
}
|
||||
}
|
||||
19
src/TerribleDev.Blog.Web/Models/PostSettings.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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!
|
||||
|
||||

|
||||
|
||||
|
||||
## 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
|
||||
@@ -19,6 +19,7 @@ namespace TerribleDev.Blog.Web
|
||||
|
||||
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
|
||||
WebHost.CreateDefaultBuilder(args)
|
||||
.UseApplicationInsights()
|
||||
.UseStartup<Startup>()
|
||||
.ConfigureKestrel(a =>
|
||||
{
|
||||
|
||||
@@ -15,20 +15,33 @@ using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using HardHat.Middlewares;
|
||||
using HardHat;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
|
||||
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.AddResponseCompression(a =>
|
||||
{
|
||||
a.EnableForHttps = true;
|
||||
@@ -57,31 +70,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();
|
||||
}
|
||||
|
||||
72
src/TerribleDev.Blog.Web/Taghelpers/InlineCss.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/TerribleDev.Blog.Web/Taghelpers/Mobile.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,13 @@
|
||||
<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="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 +39,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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@model TerribleDev.Blog.Core.Models.Post
|
||||
@inject BlogConfiguration config
|
||||
@model IPost
|
||||
@{
|
||||
ViewData["Title"] = "Post";
|
||||
ViewData["HideNav"] = true;
|
||||
@@ -13,7 +14,7 @@
|
||||
<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">
|
||||
@@ -23,10 +24,10 @@
|
||||
<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="https://blog.terribledev.io@(image)">
|
||||
}
|
||||
@if(Model.Images.Length > 0)
|
||||
@if(Model.Images.Count > 0)
|
||||
{
|
||||
<meta name="twitter:image" content="https://blog.terribledev.io@(Model.Images[0])">
|
||||
<meta name="twitter:image" content="https://blog.terribledev.io@(Model.Images[0])">
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
@model TerribleDev.Blog.Core.Models.Post
|
||||
@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 />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
@@ -7,9 +8,9 @@
|
||||
<link rel="preconnect" href="https://www.googletagmanager.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)
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
@@ -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 "TerribleDev" 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": "";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
@model Dictionary<string, List<TerribleDev.Blog.Core.Models.Post>>
|
||||
@model Dictionary<string, List<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" />
|
||||
|
||||
@@ -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
|
||||
@@ -9,5 +9,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
"AllowedHosts": "*",
|
||||
"Blog": {
|
||||
"title": "The Ramblings of TerribleDev",
|
||||
"link": "https://blog.terribledev.io"
|
||||
},
|
||||
"ApplicationInsights": {
|
||||
"InstrumentationKey": "974b47d2-1f08-42df-b498-bbfda7425f0b"
|
||||
}
|
||||
}
|
||||
BIN
src/TerribleDev.Blog.Web/wwwroot/Banner.jpg.webp
Normal file
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 12 KiB |