Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a74dec639a | ||
|
|
f93aea2696 | ||
|
|
63828b3003 | ||
|
|
1b372b3501 | ||
|
|
5edba65844 | ||
|
|
afcae26bc1 | ||
|
|
5a253f6ada | ||
|
|
e8c548ab20 | ||
|
|
0871e037d8 | ||
|
|
a5931f48c6 | ||
|
|
2cff53f5d3 | ||
|
|
e065871145 | ||
|
|
5cf4086872 | ||
|
|
d3638b10c0 | ||
|
|
bd790926b3 | ||
|
|
2753099f72 | ||
|
|
20dc7ad932 | ||
|
|
3892cb578e | ||
|
|
c6ee8f8193 | ||
|
|
6147b840f2 | ||
|
|
d032ffcf82 | ||
|
|
ac28c642f8 | ||
|
|
078da9731b | ||
|
|
e8e9a1caa7 | ||
|
|
cde154ee3b | ||
|
|
f97bc8d938 | ||
|
|
72824b70a0 | ||
|
|
87f50e1324 | ||
|
|
b316cc7e8e | ||
|
|
cc34f198a8 | ||
|
|
c1687cccf5 | ||
|
|
e6d7240996 | ||
|
|
6b9e0c8fe3 | ||
|
|
910a5fee16 | ||
|
|
6aec3294dc |
@@ -4,6 +4,6 @@
|
||||
.gitignore
|
||||
.vs
|
||||
.vscode
|
||||
*/bin
|
||||
*/obj
|
||||
**/bin
|
||||
**/obj
|
||||
**/.toolstarget
|
||||
42
.github/workflows/blogcontainergroup-AutoDeployTrigger-ab8fcfc6-eced-47ac-8584-4f5a983b4ee2.yml
vendored
Normal file
42
.github/workflows/blogcontainergroup-AutoDeployTrigger-ab8fcfc6-eced-47ac-8584-4f5a983b4ee2.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Trigger auto deployment for blogcontainergroup
|
||||
|
||||
# When this action will be executed
|
||||
on:
|
||||
# Automatically trigger it when detected changes in repo
|
||||
push:
|
||||
branches:
|
||||
[ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '.github/workflows/blogcontainergroup-AutoDeployTrigger-ab8fcfc6-eced-47ac-8584-4f5a983b4ee2.yml'
|
||||
|
||||
# Allow mannually trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout to the branch
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Azure Login
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
creds: ${{ secrets.BLOGCONTAINERGROUP_AZURE_CREDENTIALS }}
|
||||
|
||||
- name: Build and push container image to registry
|
||||
uses: azure/container-apps-deploy-action@v1
|
||||
with:
|
||||
appSourcePath: ${{ github.workspace }}
|
||||
registryUrl: terribledevreg.azurecr.io
|
||||
registryUsername: ${{ secrets.BLOGCONTAINERGROUP_REGISTRY_USERNAME }}
|
||||
registryPassword: ${{ secrets.BLOGCONTAINERGROUP_REGISTRY_PASSWORD }}
|
||||
containerAppName: blogcontainergroup
|
||||
resourceGroup: ContainerGroup
|
||||
imageToBuild: terribledevreg.azurecr.io/blogcontainergroup:${{ github.sha }}
|
||||
|
||||
|
||||
|
||||
|
||||
51
.github/workflows/master_tparnellbloglinux.yml
vendored
51
.github/workflows/master_tparnellbloglinux.yml
vendored
@@ -1,51 +0,0 @@
|
||||
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
|
||||
# More GitHub Actions for Azure: https://github.com/Azure/actions
|
||||
|
||||
name: Build and deploy container app to Azure Web App - tparnellbloglinux
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: https://terribledevreg.azurecr.io/
|
||||
username: ${{ secrets.AzureAppService_ContainerUsername_aec4619fe53744eab156fa2356a5e1e4 }}
|
||||
password: ${{ secrets.AzureAppService_ContainerPassword_1a9d2b89f86245f982b0fbfc81951798 }}
|
||||
|
||||
- name: Build and push container image to registry
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: true
|
||||
tags: terribledevreg.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_aec4619fe53744eab156fa2356a5e1e4 }}/tparnellbloglinux-img:${{ github.sha }}
|
||||
file: ./Dockerfile
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
environment:
|
||||
name: 'production'
|
||||
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
|
||||
|
||||
steps:
|
||||
- name: Deploy to Azure Web App
|
||||
id: deploy-to-webapp
|
||||
uses: azure/webapps-deploy@v2
|
||||
with:
|
||||
app-name: 'tparnellbloglinux'
|
||||
slot-name: 'production'
|
||||
publish-profile: ${{ secrets.AzureAppService_PublishProfile_110e4da4c4b44f4fbd30c6811b6cb64c }}
|
||||
images: 'terribledevreg.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_aec4619fe53744eab156fa2356a5e1e4 }}/tparnellbloglinux-img:${{ github.sha }}'
|
||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -9,7 +9,7 @@
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/src/TerribleDev.Blog.Web/bin/Debug/netcoreapp3.1/TerribleDev.Blog.Web.dll",
|
||||
"program": "${workspaceFolder}/src/TerribleDev.Blog.Web/bin/Debug/net7.0/TerribleDev.Blog.Web.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/TerribleDev.Blog.Web",
|
||||
"stopAtEntry": false,
|
||||
|
||||
37
Dockerfile
37
Dockerfile
@@ -1,17 +1,26 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
|
||||
# https://hub.docker.com/_/microsoft-dotnet
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build
|
||||
WORKDIR /source
|
||||
|
||||
# copy csproj and restore as distinct layers
|
||||
COPY ./src/TerribleDev.Blog.Web/*.csproj .
|
||||
RUN dotnet restore -r linux-musl-x64 /p:PublishReadyToRunComposite=true
|
||||
|
||||
# copy everything else and build app
|
||||
COPY ./src/TerribleDev.Blog.Web/ .
|
||||
RUN dotnet publish -c release -o /app -r linux-musl-x64 --self-contained true --no-restore /p:PublishTrimmed=true /p:PublishReadyToRunComposite=true /p:PublishSingleFile=true
|
||||
RUN date +%s > /app/buildtime.txt
|
||||
# final stage/image
|
||||
FROM mcr.microsoft.com/dotnet/runtime-deps:7.0-alpine-amd64
|
||||
WORKDIR /app
|
||||
COPY --from=build /app ./
|
||||
|
||||
# Copy csproj and restore as distinct layers
|
||||
COPY *.sln .
|
||||
COPY . .
|
||||
RUN dotnet restore
|
||||
# See: https://github.com/dotnet/announcements/issues/20
|
||||
# Uncomment to enable globalization APIs (or delete)
|
||||
# ENV \
|
||||
# DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \
|
||||
# LC_ALL=en_US.UTF-8 \
|
||||
# LANG=en_US.UTF-8
|
||||
# RUN apk add --no-cache icu-libs
|
||||
|
||||
# Copy everything else and build
|
||||
COPY . .
|
||||
RUN dotnet publish -c Release -o out
|
||||
|
||||
# Build runtime image
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0
|
||||
WORKDIR /app
|
||||
COPY --from=build-env /app/out .
|
||||
ENTRYPOINT ["dotnet", "TerribleDev.Blog.Web.dll"]
|
||||
ENTRYPOINT ["./TerribleDev.Blog.Web"]
|
||||
12
Dockerfile.old
Normal file
12
Dockerfile.old
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Copy everything else and build
|
||||
COPY /src/TerribleDev.Blog.Web .
|
||||
RUN dotnet publish -c release -o /out -r linux-musl-x64 --self-contained true /p:PublishTrimmed=true /p:PublishReadyToRunComposite=true /p:PublishSingleFile=true
|
||||
RUN date +%s > /out/buildtime.txt
|
||||
# Build runtime image
|
||||
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0-alpine-amd64
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/out .
|
||||
ENTRYPOINT ["./TerribleDev.Blog.Web"]
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"dockerfilePath": "./Dockerfile"
|
||||
}
|
||||
38
fly.toml
Normal file
38
fly.toml
Normal file
@@ -0,0 +1,38 @@
|
||||
# fly.toml file generated for dry-meadow-9911 on 2022-11-09T12:09:05-05:00
|
||||
|
||||
app = "dry-meadow-9911"
|
||||
kill_signal = "SIGINT"
|
||||
kill_timeout = 5
|
||||
processes = []
|
||||
|
||||
[env]
|
||||
|
||||
[experimental]
|
||||
allowed_public_ports = []
|
||||
auto_rollback = true
|
||||
|
||||
[[services]]
|
||||
http_checks = []
|
||||
internal_port = 80
|
||||
processes = ["app"]
|
||||
protocol = "tcp"
|
||||
script_checks = []
|
||||
[services.concurrency]
|
||||
hard_limit = 25
|
||||
soft_limit = 20
|
||||
type = "connections"
|
||||
|
||||
[[services.ports]]
|
||||
force_https = true
|
||||
handlers = ["http"]
|
||||
port = 80
|
||||
|
||||
# [[services.ports]]
|
||||
# handlers = ["tls", "http"]
|
||||
# port = 443
|
||||
|
||||
[[services.tcp_checks]]
|
||||
grace_period = "2s"
|
||||
interval = "3s"
|
||||
restart_limit = 0
|
||||
timeout = "2s"
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp6.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp7.0</TargetFramework>
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackAsTool>true</PackAsTool>
|
||||
<ToolCommandName>tempo</ToolCommandName>
|
||||
|
||||
@@ -9,16 +9,19 @@ using System.IO;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using TerribleDev.Blog.Web.Filters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Controllers
|
||||
{
|
||||
[Http2PushFilter]
|
||||
public class HomeController : Controller
|
||||
{
|
||||
private readonly ILogger<HomeController> logger;
|
||||
private readonly PostCache postCache;
|
||||
|
||||
public HomeController(PostCache postCache)
|
||||
public HomeController(PostCache postCache, ILogger<HomeController> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.postCache = postCache;
|
||||
}
|
||||
|
||||
@@ -27,18 +30,26 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
[Route("/index.html", Order = 2)]
|
||||
[Route("/")]
|
||||
[Route("/page/{pageNumber:required:int:min(1)}")]
|
||||
[OutputCache(Duration = 31536000, VaryByParam = "pageNumber")]
|
||||
[OutputCache(Duration = 31536000, VaryByRouteValueNames = new string[] { "pageNumber" })]
|
||||
[ResponseCache(Duration = 900)]
|
||||
public IActionResult Index(int pageNumber = 1)
|
||||
{
|
||||
if(!postCache.PostsByPage.TryGetValue(pageNumber, out var result))
|
||||
this.logger.LogWarning("Viewing page", pageNumber);
|
||||
if (!postCache.PostsByPage.TryGetValue(pageNumber, out var result))
|
||||
{
|
||||
return Redirect($"/404/?from=/page/{pageNumber}/");
|
||||
}
|
||||
return View(new HomeViewModel() { Posts = result, Page = pageNumber, HasNext = postCache.PostsByPage.ContainsKey(pageNumber + 1), HasPrevious = postCache.PostsByPage.ContainsKey(pageNumber - 1),
|
||||
BlogLD = postCache.BlogLD,
|
||||
SiteLD = postCache.SiteLD,
|
||||
BlogLDString = postCache.BlogLDString, SiteLDString = postCache.SiteLDString });
|
||||
return View(new HomeViewModel()
|
||||
{
|
||||
Posts = result,
|
||||
Page = pageNumber,
|
||||
HasNext = postCache.PostsByPage.ContainsKey(pageNumber + 1),
|
||||
HasPrevious = postCache.PostsByPage.ContainsKey(pageNumber - 1),
|
||||
BlogLD = postCache.BlogLD,
|
||||
SiteLD = postCache.SiteLD,
|
||||
BlogLDString = postCache.BlogLDString,
|
||||
SiteLDString = postCache.SiteLDString
|
||||
});
|
||||
}
|
||||
[Route("/theme/{postName?}")]
|
||||
public IActionResult Theme(string postName)
|
||||
@@ -60,29 +71,34 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
}
|
||||
|
||||
[Route("{postUrl}/{amp?}")]
|
||||
[OutputCache(Duration = 31536000, VaryByParam = "postUrl,amp")]
|
||||
[OutputCache(Duration = 31536000, VaryByRouteValueNames = new string[] { "postUrl", "amp" })]
|
||||
[ResponseCache(Duration = 900)]
|
||||
public IActionResult Post(string postUrl, string amp = "")
|
||||
{
|
||||
if(!String.IsNullOrEmpty(amp) && amp != "amp")
|
||||
if (!String.IsNullOrEmpty(amp) && amp != "amp")
|
||||
{
|
||||
return Redirect($"/404/?from=/{postUrl}/{amp}/");
|
||||
}
|
||||
var isAmp = amp == "amp";
|
||||
this.ViewData["amp"] = isAmp;
|
||||
if(postCache.UrlToPost.TryGetValue(postUrl, out var currentPost))
|
||||
if (isAmp)
|
||||
{
|
||||
if(isAmp && !currentPost.isAmp)
|
||||
{
|
||||
return Redirect($"/{postUrl}/");
|
||||
}
|
||||
return View("Post", model: new PostViewModel() { Post = currentPost });
|
||||
return this.RedirectPermanent($"/{postUrl}");
|
||||
}
|
||||
if(postCache.LandingPagesUrl.TryGetValue(postUrl, out var landingPage))
|
||||
// case sensitive lookup
|
||||
if (postCache.UrlToPost.TryGetValue(postUrl, out var currentPost))
|
||||
{
|
||||
return View("Post", model: new PostViewModel() { Post = landingPage });
|
||||
return View("Post", model: new PostViewModel() { Post = currentPost });
|
||||
}
|
||||
|
||||
// case insensitive lookup on post
|
||||
if (postCache.CaseInsensitiveUrlToPost.TryGetValue(postUrl, out var caseInsensitivePost))
|
||||
{
|
||||
return View("Post", model: new PostViewModel() { Post = caseInsensitivePost });
|
||||
}
|
||||
if (postCache.LandingPagesUrl.TryGetValue(postUrl, out var landingPage))
|
||||
{
|
||||
return View("Post", model: new PostViewModel() { Post = landingPage });
|
||||
}
|
||||
|
||||
this.StatusCode(404);
|
||||
return View(nameof(FourOhFour));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Microsoft.SyndicationFeed;
|
||||
using Microsoft.SyndicationFeed.Rss;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
@@ -66,7 +67,6 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
};
|
||||
sitemap.Urls.AddRange(postCache.TagsToPosts.Keys.Select(i => new SiteMapItem() { LastModified = DateTime.UtcNow, Location = $"https://blog.terrible.dev/search?q={i}" }));
|
||||
sitemap.Urls.AddRange(sitewideLinks);
|
||||
sitemap.Urls.AddRange(postCache.PostsAsLists.Where(i => i.isAmp).Select(a => new SiteMapItem() { LastModified = DateTime.UtcNow, Location = a.AMPUrl }).ToList());
|
||||
ser.Serialize(this.Response.Body, sitemap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using TerribleDev.Blog.Web.Filters;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
|
||||
@@ -24,7 +25,7 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
return View(postCache.TagsToPosts);
|
||||
}
|
||||
[Route("/tags/{tagName}")]
|
||||
[OutputCache(Duration = 31536000, VaryByParam = "tagName")]
|
||||
[OutputCache(Duration = 31536000, VaryByRouteValueNames = new string[]{"tagName"})]
|
||||
public IActionResult TagPluralRedirect(string tagName)
|
||||
{
|
||||
if(string.IsNullOrEmpty(tagName))
|
||||
@@ -34,7 +35,7 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
return Redirect($"/tag/{tagName}/");
|
||||
}
|
||||
[Route("/tag/{tagName}")]
|
||||
[OutputCache(Duration = 31536000, VaryByParam = "tagName")]
|
||||
[OutputCache(Duration = 31536000, VaryByRouteValueNames = new string[] {"tagName"})]
|
||||
public IActionResult GetTag(string tagName)
|
||||
{
|
||||
if(!postCache.TagsToPosts.TryGetValue(tagName.ToLower(), out var models))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# https://hub.docker.com/_/microsoft-dotnet
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build
|
||||
WORKDIR /source
|
||||
|
||||
# copy csproj and restore as distinct layers
|
||||
@@ -11,7 +11,7 @@ COPY . .
|
||||
RUN dotnet publish -c release -o /app -r linux-musl-x64 --self-contained true --no-restore /p:PublishTrimmed=true /p:PublishReadyToRunComposite=true /p:PublishSingleFile=true
|
||||
|
||||
# final stage/image
|
||||
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0-alpine-amd64
|
||||
FROM mcr.microsoft.com/dotnet/runtime-deps:7.0-alpine-amd64
|
||||
WORKDIR /app
|
||||
COPY --from=build /app ./
|
||||
|
||||
|
||||
12
src/TerribleDev.Blog.Web/Extensions/ArrayExtensions.cs
Normal file
12
src/TerribleDev.Blog.Web/Extensions/ArrayExtensions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public static class ArrayExtensions
|
||||
{
|
||||
public static string ToHexString(this byte[] bytes)
|
||||
{
|
||||
return Convert.ToHexString(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ namespace TerribleDev.Blog.Web.Factories
|
||||
var orderedPosts = rawPosts.OrderByDescending(a => a.PublishDate);
|
||||
var posts = new List<IPost>(orderedPosts);
|
||||
var urlToPosts = new Dictionary<string, IPost>();
|
||||
var caseInsensitiveUrlToPost = new Dictionary<string, IPost>(StringComparer.OrdinalIgnoreCase);
|
||||
var tagsToPost = new Dictionary<string, IList<Post>>();
|
||||
var postsByPage = new Dictionary<int, IList<Post>>();
|
||||
var syndicationPosts = new List<SyndicationItem>();
|
||||
@@ -30,6 +31,7 @@ namespace TerribleDev.Blog.Web.Factories
|
||||
{
|
||||
var castedPost = post as Post;
|
||||
urlToPosts.Add(post.UrlWithoutPath, castedPost);
|
||||
caseInsensitiveUrlToPost.Add(post.UrlWithoutPath.ToLower(), castedPost);
|
||||
syndicationPosts.Add(castedPost.ToSyndicationItem());
|
||||
blogPostsLD.Add(post.Content.JsonLD);
|
||||
foreach (var tag in castedPost.ToNormalizedTagList())
|
||||
@@ -122,8 +124,8 @@ namespace TerribleDev.Blog.Web.Factories
|
||||
BlogLD = ld,
|
||||
SiteLD = website,
|
||||
BlogLDString = ld.ToHtmlEscapedString().Replace("https://schema.org", "https://schema.org/true"),
|
||||
SiteLDString = website.ToHtmlEscapedString().Replace("https://schema.org", "https://schema.org/true")
|
||||
|
||||
SiteLDString = website.ToHtmlEscapedString().Replace("https://schema.org", "https://schema.org/true"),
|
||||
CaseInsensitiveUrlToPost = caseInsensitiveUrlToPost
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ using System.Collections.Concurrent;
|
||||
using Schema.NET;
|
||||
using System.Text.RegularExpressions;
|
||||
using TerribleDev.Blog.Web.Factories;
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
@@ -74,11 +76,10 @@ namespace TerribleDev.Blog.Web
|
||||
var postSettings = ParseYaml(ymlRaw);
|
||||
var resolvedUrl = !string.IsNullOrWhiteSpace(postSettings.permalink) ? postSettings.permalink : fileName.Split('.')[0].Replace(' ', '-').WithoutSpecialCharacters();
|
||||
var canonicalUrl = $"https://blog.terrible.dev/{resolvedUrl}/";
|
||||
var ampUrl = $"https://blog.terrible.dev/{resolvedUrl}/amp/";
|
||||
return postSettings.isLanding ? await BuildLandingPage(fileName, domain, markdownText, postSettings, resolvedUrl, canonicalUrl, ampUrl) : await BuildPost(fileName, domain, markdownText, postSettings, resolvedUrl, canonicalUrl, ampUrl);
|
||||
return postSettings.isLanding ? await BuildLandingPage(fileName, domain, markdownText, postSettings, resolvedUrl, canonicalUrl) : await BuildPost(fileName, domain, markdownText, postSettings, resolvedUrl, canonicalUrl);
|
||||
}
|
||||
|
||||
private async Task<Post> BuildPost(string fileName, string domain, string markdownText, PostSettings postSettings, string resolvedUrl, string canonicalUrl, string ampUrl)
|
||||
private async Task<Post> BuildPost(string fileName, string domain, string markdownText, PostSettings postSettings, string resolvedUrl, string canonicalUrl)
|
||||
{
|
||||
|
||||
(string postContent, string postContentPlain, string summary, string postSummaryPlain, IList<string> postImages, bool hasCode) = await ResolveContentForPost(markdownText, fileName, resolvedUrl, domain);
|
||||
@@ -112,7 +113,6 @@ namespace TerribleDev.Blog.Web
|
||||
var postContentClean = Regex.Replace(postContent, "<picture.*?>|</picture>|<source.*?>|</source>", "", RegexOptions.Singleline);
|
||||
var content = new PostContent()
|
||||
{
|
||||
AmpContent = new HtmlString(postContentClean),
|
||||
Content = new HtmlString(postContent),
|
||||
Images = postImages,
|
||||
ContentPlain = postContentPlain,
|
||||
@@ -123,7 +123,8 @@ namespace TerribleDev.Blog.Web
|
||||
JsonLDString = ld.ToHtmlEscapedString().Replace("https://schema.org", "https://schema.org/true"),
|
||||
JsonLDBreadcrumb = breadcrumb,
|
||||
JsonLDBreadcrumbString = breadcrumb.ToHtmlEscapedString().Replace("https://schema.org", "https://schema.org/true"),
|
||||
HasCode = hasCode
|
||||
HasCode = hasCode,
|
||||
MarkdownMD5 = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(markdownText)).ToHexString()
|
||||
};
|
||||
var thumbNailUrl = string.IsNullOrWhiteSpace(postSettings.thumbnailImage) ?
|
||||
postImages?.FirstOrDefault() ?? "https://www.gravatar.com/avatar/333e3cea32cd17ff2007d131df336061?s=640" :
|
||||
@@ -136,15 +137,13 @@ namespace TerribleDev.Blog.Web
|
||||
Title = postSettings.title,
|
||||
RelativeUrl = $"/{resolvedUrl}/",
|
||||
CanonicalUrl = canonicalUrl,
|
||||
AMPUrl = ampUrl,
|
||||
UrlWithoutPath = resolvedUrl,
|
||||
isLanding = postSettings.isLanding,
|
||||
Content = content,
|
||||
isAmp = postSettings.isAmp,
|
||||
ThumbnailImage = thumbNailUrl,
|
||||
};
|
||||
}
|
||||
private async Task<LandingPage> BuildLandingPage(string fileName, string domain, string markdownText, PostSettings postSettings, string resolvedUrl, string canonicalUrl, string ampUrl)
|
||||
private async Task<LandingPage> BuildLandingPage(string fileName, string domain, string markdownText, PostSettings postSettings, string resolvedUrl, string canonicalUrl)
|
||||
{
|
||||
(string postContent, string postContentPlain, string summary, string postSummaryPlain, IList<string> postImages, bool hasCode) = await ResolveContentForPost(markdownText, fileName, resolvedUrl, domain);
|
||||
var breadcrumb = new Schema.NET.BreadcrumbList()
|
||||
@@ -164,10 +163,8 @@ namespace TerribleDev.Blog.Web
|
||||
},
|
||||
};
|
||||
// regex remove picture and source tags but not the child elements
|
||||
var postContentClean = Regex.Replace(postContent, "<picture.*?>|</picture>|<source.*?>|</source>", "", RegexOptions.Singleline);
|
||||
var content = new PostContent()
|
||||
{
|
||||
AmpContent = new HtmlString(postContentClean),
|
||||
Content = new HtmlString(postContent),
|
||||
Images = postImages,
|
||||
ContentPlain = postContentPlain,
|
||||
@@ -176,7 +173,8 @@ namespace TerribleDev.Blog.Web
|
||||
SummaryPlainShort = (postContentPlain.Length <= 147 ? postContentPlain : postContentPlain.Substring(0, 146)) + "...",
|
||||
JsonLDBreadcrumb = breadcrumb,
|
||||
JsonLDBreadcrumbString = breadcrumb.ToHtmlEscapedString().Replace("https://schema.org", "https://schema.org/true"),
|
||||
HasCode = hasCode
|
||||
HasCode = hasCode,
|
||||
MarkdownMD5 = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(markdownText)).ToHexString()
|
||||
};
|
||||
return new LandingPage()
|
||||
{
|
||||
@@ -185,11 +183,9 @@ namespace TerribleDev.Blog.Web
|
||||
Title = postSettings.title,
|
||||
RelativeUrl = $"/{resolvedUrl}/",
|
||||
CanonicalUrl = canonicalUrl,
|
||||
AMPUrl = ampUrl,
|
||||
UrlWithoutPath = resolvedUrl,
|
||||
isLanding = postSettings.isLanding,
|
||||
Content = content,
|
||||
isAmp = postSettings.isAmp
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,13 @@ namespace TerribleDev.Blog.Web.Factories
|
||||
public class CodeFactory
|
||||
{
|
||||
private HttpClient httpClient = new HttpClient();
|
||||
private static Boolean IsDisabled = !String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DISABLE_PRISMA"));
|
||||
public async Task<(string result, bool hasCode)> ReplaceFencedCode(string markdown)
|
||||
{
|
||||
if(CodeFactory.IsDisabled)
|
||||
{
|
||||
return (markdown, false);
|
||||
}
|
||||
|
||||
// regex grab all text between backticks
|
||||
var regex = new Regex(@"```(.*?)```", RegexOptions.Singleline);
|
||||
|
||||
43
src/TerribleDev.Blog.Web/Filters/ETagFilter.cs
Normal file
43
src/TerribleDev.Blog.Web/Filters/ETagFilter.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Filters
|
||||
{
|
||||
public class StaticETag: ActionFilterAttribute
|
||||
{
|
||||
static StaticETag()
|
||||
{
|
||||
string etagString;
|
||||
if(File.Exists("buildtime.txt"))
|
||||
{
|
||||
Console.WriteLine("buildtime.txt found");
|
||||
etagString = File.ReadAllText("buildtime.txt");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("buildtime.txt not found");
|
||||
Console.WriteLine("Directory list");
|
||||
Console.WriteLine(Directory.GetFiles(".", "*", SearchOption.AllDirectories).Aggregate((a, b) => a + "\n" + b));
|
||||
var unixTime = DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString();
|
||||
Console.WriteLine("Using Unix Time for Etag: " + unixTime);
|
||||
etagString = unixTime;
|
||||
}
|
||||
StaticETag.staticEtag = "\"" + MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(etagString)).ToHexString().Substring(0,8) + "\"";
|
||||
}
|
||||
public static string staticEtag;
|
||||
public static ConcurrentDictionary<string, string> cache = new ConcurrentDictionary<string, string>();
|
||||
public override void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
if(context.HttpContext.Response.StatusCode >= 200 && context.HttpContext.Response.StatusCode < 300 && context.HttpContext.Response.Headers.ETag.Count == 0)
|
||||
{
|
||||
context.HttpContext.Response.Headers.Add("ETag", staticEtag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,13 @@ namespace TerribleDev.Blog.Web.Filters
|
||||
{
|
||||
public class Http2PushFilter : ActionFilterAttribute
|
||||
{
|
||||
public override void OnResultExecuted(ResultExecutedContext context)
|
||||
private static bool IsHttp2PushDisabled = String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DISABLE_HTTP2_PUSH"));
|
||||
public override void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
if(IsHttp2PushDisabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var logger = context.HttpContext.RequestServices.GetService(typeof(ILogger<Http2PushFilter>)) as ILogger<Http2PushFilter>;
|
||||
logger.LogDebug("Http2PushFilter.OnActionExecuted");
|
||||
if(!context.HttpContext.Items.TryGetValue(HttpPush.Key, out var links))
|
||||
@@ -17,22 +22,23 @@ namespace TerribleDev.Blog.Web.Filters
|
||||
logger.LogDebug("Did not find any links to push");
|
||||
return;
|
||||
}
|
||||
var linkData = links as System.Collections.Generic.List<string>;
|
||||
var linkData = links as System.Collections.Generic.List<PushUrl>;
|
||||
if(linkData == null || linkData.Count == 0) {
|
||||
logger.LogDebug("Http2PushFilter.OnActionExecuted: No links");
|
||||
return;
|
||||
}
|
||||
var headerBuilder = new StringBuilder();
|
||||
for(var i = 0; i < linkData.Count; i++) {
|
||||
var url = linkData[i];
|
||||
var (url, AsProperty) = linkData[i];
|
||||
var resolvedUrl = url.StartsWith("~") ? context.HttpContext.Request.PathBase.ToString() + url.Substring(1) : url;
|
||||
headerBuilder.Append($"<{resolvedUrl}>; rel=preload; as=style");
|
||||
headerBuilder.Append($"<{resolvedUrl}>; rel=preload; as={AsProperty}");
|
||||
if(i < linkData.Count - 1) {
|
||||
headerBuilder.Append(", ");
|
||||
}
|
||||
}
|
||||
logger.LogDebug("Http2PushFilter.OnActionExecuted: " + headerBuilder.ToString());
|
||||
context.HttpContext.Response.Headers.Add("Link", headerBuilder.ToString());
|
||||
base.OnActionExecuted(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,10 @@ namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if(linkInline.Url.EndsWith(".gif"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
renderer.Write("<picture>");
|
||||
WriteImageTag(renderer, linkInline, ".webp", "image/webp");
|
||||
|
||||
@@ -9,7 +9,6 @@ namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public interface IPost
|
||||
{
|
||||
string AMPUrl { get; set; }
|
||||
string CanonicalUrl { get; set; }
|
||||
string UrlWithoutPath { get; set; }
|
||||
string RelativeUrl { get; set; }
|
||||
@@ -18,7 +17,6 @@ namespace TerribleDev.Blog.Web.Models
|
||||
DateTime? UpdatedDate { get; set; }
|
||||
IPostContent Content { get; set; }
|
||||
bool isLanding { get; set; }
|
||||
bool isAmp { get; set; }
|
||||
string ThumbnailImage { get; }
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public interface IPostContent
|
||||
{
|
||||
public HtmlString AmpContent { get; set; }
|
||||
HtmlString Content { get; set; }
|
||||
HtmlString Summary { get; set; }
|
||||
string ContentPlain { get; set; }
|
||||
@@ -22,5 +21,6 @@ namespace TerribleDev.Blog.Web.Models
|
||||
public string JsonLDString { get; set; }
|
||||
BreadcrumbList JsonLDBreadcrumb { get; set; }
|
||||
string JsonLDBreadcrumbString { get; set; }
|
||||
string MarkdownMD5 { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ namespace TerribleDev.Blog.Web.Models
|
||||
[DebuggerDisplay("{Title}")]
|
||||
public class LandingPage : IPost
|
||||
{
|
||||
public string AMPUrl { get; set; }
|
||||
public string CanonicalUrl { get; set; }
|
||||
public string UrlWithoutPath { get; set; }
|
||||
public string RelativeUrl { get; set; }
|
||||
@@ -19,7 +18,6 @@ namespace TerribleDev.Blog.Web.Models
|
||||
public IPostContent Content { get; set; }
|
||||
|
||||
public bool isLanding { get; set; } = false;
|
||||
public bool isAmp { get; set; } = true;
|
||||
public string ThumbnailImage { get => "https://www.gravatar.com/avatar/333e3cea32cd17ff2007d131df336061?s=640"; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ namespace TerribleDev.Blog.Web.Models
|
||||
[DebuggerDisplay("{Title}")]
|
||||
public class Post : IPost
|
||||
{
|
||||
public string AMPUrl { get; set; }
|
||||
public string CanonicalUrl { get; set; }
|
||||
public string UrlWithoutPath { get; set; }
|
||||
public string RelativeUrl { get; set; }
|
||||
@@ -20,7 +19,6 @@ namespace TerribleDev.Blog.Web.Models
|
||||
public IPostContent Content { get; set; }
|
||||
|
||||
public bool isLanding { get; set; } = false;
|
||||
public bool isAmp { get; set; } = true;
|
||||
|
||||
public string ThumbnailImage { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace TerribleDev.Blog.Web.Models
|
||||
public IList<IPost> PostsAsLists { get; set;}
|
||||
public IDictionary<string, IList<Post>> TagsToPosts { get; set; }
|
||||
public IDictionary<string, IPost> UrlToPost { get; set; }
|
||||
public IDictionary<string, IPost> CaseInsensitiveUrlToPost { get; set; }
|
||||
public IDictionary<int, IList<Post>> PostsByPage { get; set; }
|
||||
public IList<SyndicationItem> PostsAsSyndication { get; set; }
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace TerribleDev.Blog.Web.Models
|
||||
|
||||
public class PostContent : IPostContent
|
||||
{
|
||||
public HtmlString AmpContent { get; set; }
|
||||
public HtmlString Content { get; set; }
|
||||
public HtmlString Summary { get; set; }
|
||||
public string ContentPlain { get; set; }
|
||||
@@ -20,5 +19,6 @@ namespace TerribleDev.Blog.Web.Models
|
||||
public BreadcrumbList JsonLDBreadcrumb { get; set; }
|
||||
public string JsonLDBreadcrumbString { get; set; }
|
||||
public bool HasCode { get; set; }
|
||||
public string MarkdownMD5 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,5 @@ namespace TerribleDev.Blog.Web.Models
|
||||
|
||||
public bool isLanding { get; set; } = false;
|
||||
|
||||
public bool isAmp { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
title: Building attractive CLIs in TypeScript
|
||||
date: 2022-07-08 05:18
|
||||
tags:
|
||||
- javascript
|
||||
- typescript
|
||||
- node
|
||||
- cli
|
||||
- tutorials
|
||||
---
|
||||
|
||||
So you've come to a point where you want to build nice CLIs. There's a few different options for building CLI's. My two favorites are [oclif](https://oclif.io/) and [commander.js](https://github.com/tj/commander.js/). I tend toward leaning to commander, unless I know I'm building a super big app. However, I've really enjoyed building smaller CLIs with commander recently.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
> tl;dr? You can [view this repo](https://github.com/TerribleDev/example-ts-cli)
|
||||
|
||||

|
||||
|
||||
## Commander.js Lingo
|
||||
|
||||
So commander has a few different nouns.
|
||||
|
||||
* `Program` - The root of the CLI. Handles running the core app.
|
||||
* `Command` - A command that can be run. These must be registered into `Program`
|
||||
* `Option` - I would also call these `flags` they're the `--something` part of the CLI.
|
||||
* `Arguments` - These are named positioned arguments. For example `npm install commander` the `commander` string in this case is an argument. `--save` would be an option.
|
||||
|
||||
|
||||
|
||||
## Initial Setup
|
||||
|
||||
First, do an npm init, and install commander, types for node, typescript, esbuild, and optionally ora.
|
||||
|
||||
```bash
|
||||
npm init -y
|
||||
npm install --save commander typescript @types/node ora
|
||||
```
|
||||
|
||||
Next we have to configure a build command in the package.json. This one runs typescript to check for types and then esbuild to compile the app for node.
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit ./index.ts && esbuild index.ts --bundle --platform=node --format=cjs --outfile=dist/index.js",
|
||||
}
|
||||
```
|
||||
|
||||
We now need to add a bin property in the package.json. This tells the package manager that we have an executable. The key should be the name of your CLI
|
||||
|
||||
```json
|
||||
"bin": {
|
||||
"<yourclinamehere>": "./dist/index.js"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Make a file called index.ts, and place this string on the first line. This is called a shebang and it tells your shell to use node when the file is ran.
|
||||
|
||||
`#!/usr/bin/env node`
|
||||
|
||||
## Getting started
|
||||
|
||||
Hopefully you have done the above. Now in index.ts you can make a very basic program. Try npm build and then run the CLI with --help. Hopefully you'll get some output.
|
||||
|
||||
```ts
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from 'commander'
|
||||
import { spinnerError, stopSpinner } from './spinner';
|
||||
const program = new Command();
|
||||
program.description('Our New CLI');
|
||||
program.version('0.0.1');
|
||||
|
||||
async function main() {
|
||||
await program.parseAsync();
|
||||
|
||||
}
|
||||
console.log() // log a new line so there is a nice space
|
||||
main();
|
||||
```
|
||||
|
||||
### Setting up the spinner
|
||||
|
||||
So, I really like loading spinners. I think it gives the CLI a more polished feel. So I added a spinner using ora. I made a file called `spinner.ts` which is a wrapper to handle states of spinning or stopped.
|
||||
|
||||
```ts
|
||||
import ora from 'ora';
|
||||
|
||||
const spinner = ora({ // make a singleton so we don't ever have 2 spinners
|
||||
spinner: 'dots',
|
||||
})
|
||||
|
||||
export const updateSpinnerText = (message: string) => {
|
||||
if(spinner.isSpinning) {
|
||||
spinner.text = message
|
||||
return;
|
||||
}
|
||||
spinner.start(message)
|
||||
}
|
||||
|
||||
export const stopSpinner = () => {
|
||||
if(spinner.isSpinning) {
|
||||
spinner.stop()
|
||||
}
|
||||
}
|
||||
export const spinnerError = (message?: string) => {
|
||||
if(spinner.isSpinning) {
|
||||
spinner.fail(message)
|
||||
}
|
||||
}
|
||||
export const spinnerSuccess = (message?: string) => {
|
||||
if(spinner.isSpinning) {
|
||||
spinner.succeed(message)
|
||||
}
|
||||
}
|
||||
export const spinnerInfo = (message: string) => {
|
||||
spinner.info(message)
|
||||
}
|
||||
```
|
||||
|
||||
### Writing a command
|
||||
|
||||
So I like to separate my commands out into sub-commands. In this case we're making `widgets` a sub-command. Make a new file, I call it widgets.ts. I create a new `Command` called `widgets`. Commands can have commands making them sub-commands. So we can make a sub-command called `list` and `get`. **List** will list all the widgets we have, and **get** will retrive a widget by id. I added some promise to emulate some delay so we can see the spinner in action.
|
||||
|
||||
|
||||
```ts
|
||||
import { Command } from "commander";
|
||||
import { spinnerError, spinnerInfo, spinnerSuccess, updateSpinnerText } from "./spinner";
|
||||
|
||||
export const widgets = new Command("widgets");
|
||||
|
||||
widgets.command("list").action(async () => {
|
||||
updateSpinnerText("Processing ");
|
||||
// do work
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // emulate work
|
||||
spinnerSuccess()
|
||||
console.table([{ id: 1, name: "Tommy" }, { id: 2, name: "Bob" }]);
|
||||
})
|
||||
|
||||
widgets.command("get")
|
||||
.argument("<id>", "the id of the widget")
|
||||
.option("-f, --format <format>", "the format of the widget") // an optional flag, this will be in options.f
|
||||
.action(async (id, options) => {
|
||||
updateSpinnerText("Getting widget " + id);
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
spinnerSuccess()
|
||||
console.table({ id: 1, name: "Tommy" })
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
Now lets register this command into our program. (see the last line)
|
||||
|
||||
```ts
|
||||
#!/usr/bin/env node
|
||||
import { Command } from 'commander'
|
||||
import { spinnerError, stopSpinner } from './spinner';
|
||||
import { widgets } from './widgets';
|
||||
const program = new Command();
|
||||
program.description('Our New CLI');
|
||||
program.version('0.0.1');
|
||||
program.addCommand(widgets);
|
||||
```
|
||||
|
||||
|
||||
Do a build! Hopefully you can type `<yourcli> widgets list` and you'll see the spinner. When you call `spinnerSuccess` without any parameters the previous spinner text will stop and become a green check. You can pass a message instead to print that to the console. You can also call `spinnerError` to make the spinner a red `x` and print the message.
|
||||
|
||||
|
||||
### Handle unhandled errors
|
||||
|
||||
Back in index.ts we need to add a hook to capture unhandled errors. Add a verbose flag to the program so we can see more details about the error, but by default lets hide the errors.
|
||||
|
||||
```ts
|
||||
const program = new Command('Our New CLI');
|
||||
program.option('-v, --verbose', 'verbose logging');
|
||||
```
|
||||
|
||||
Now we need to listen for the node unhandled promise rejection event and process it.
|
||||
|
||||
|
||||
```ts
|
||||
process.on('unhandledRejection', function (err: Error) { // listen for unhandled promise rejections
|
||||
const debug = program.opts().verbose; // is the --verbose flag set?
|
||||
if(debug) {
|
||||
console.error(err.stack); // print the stack trace if we're in verbose mode
|
||||
}
|
||||
spinnerError() // show an error spinner
|
||||
stopSpinner() // stop the spinner
|
||||
program.error('', { exitCode: 1 }); // exit with error code 1
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
#### Testing our error handling
|
||||
|
||||
Lets make a widget action called `unhandled-error`. Do a build, and then run this action. You should see the error is swallowed. Now try again but use `<yourcli> --verbose widgets unhandled-error` and you should see the error stack trace.
|
||||
|
||||
```ts
|
||||
widgets.command("unhandled-error").action(async () => {
|
||||
updateSpinnerText("Processing an unhandled failure ");
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
throw new Error("Unhandled error");
|
||||
})
|
||||
```
|
||||
|
||||
## Organizing the folders
|
||||
|
||||
Ok, so you have the basics all setup. Now, how do you organize the folders. I like to have the top level commands in their own directories. That way the folder structure emulates the CLI. This is an idea I saw in oclif.
|
||||
|
||||
```
|
||||
- index.ts
|
||||
- /commands/widgets/index.ts
|
||||
- /commands/widgets/list.ts
|
||||
- /commands/widgets/get.ts
|
||||
|
||||
```
|
||||
|
||||
## So why not OCLIF?
|
||||
|
||||
A few simple reasons. OCLIF's getting started template comes with an extremely opinionated typescript configuration. For large projects, I've found it to be incredible. However, for smaller-ish things, I've found conforming to it, a trial of turning down the linter a lot. Overall, they're both great tools. Why not both?
|
||||
@@ -1,6 +1,5 @@
|
||||
title: Dynamically changing the site-theme meta tag
|
||||
date: 2022-04-12 11:05
|
||||
isAmp: false
|
||||
thumbnailImage: 1.jpg
|
||||
tags:
|
||||
- javascript
|
||||
|
||||
@@ -12,8 +12,9 @@ using HardHat;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
using TerribleDev.Blog.Web.Factories;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using WebMarkupMin.AspNetCore6;
|
||||
using WebMarkupMin.AspNetCore7;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TerribleDev.Blog.Web.Filters;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
@@ -58,23 +59,32 @@ namespace TerribleDev.Blog.Web
|
||||
}
|
||||
return postCache;
|
||||
});
|
||||
var controllerBuilder = services.AddControllersWithViews();
|
||||
var controllerBuilder = services.AddControllersWithViews(a => {
|
||||
a.Filters.Add(new StaticETag());
|
||||
});
|
||||
#if DEBUG
|
||||
if (Env.IsDevelopment())
|
||||
{
|
||||
controllerBuilder.AddRazorRuntimeCompilation();
|
||||
}
|
||||
#endif
|
||||
services.AddResponseCompression(a =>
|
||||
services
|
||||
.AddResponseCompression(a =>
|
||||
{
|
||||
a.EnableForHttps = true;
|
||||
|
||||
})
|
||||
.AddResponseCaching()
|
||||
.AddMemoryCache();
|
||||
if(Env.IsProduction())
|
||||
{
|
||||
services.AddOutputCaching();
|
||||
}
|
||||
// if(Env.IsProduction())
|
||||
// {
|
||||
|
||||
// }
|
||||
services.AddOutputCache(a =>{
|
||||
a.AddBasePolicy(b => {
|
||||
b.Cache();
|
||||
});
|
||||
});
|
||||
services.AddWebMarkupMin(a => {
|
||||
a.AllowMinificationInDevelopmentEnvironment = true;
|
||||
a.DisablePoweredByHttpHeaders = true;
|
||||
@@ -86,19 +96,16 @@ namespace TerribleDev.Blog.Web
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
|
||||
}
|
||||
Console.WriteLine("ETag Detected As: " + StaticETag.staticEtag);
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
if (env.IsProduction())
|
||||
{
|
||||
app.UseOutputCache();
|
||||
app.UseResponseCaching();
|
||||
}
|
||||
app.UseResponseCompression();
|
||||
var cacheTime = env.IsDevelopment() ? 0 : 31536000;
|
||||
var cacheTime = env.IsDevelopment() ? 1 : 31536000;
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
OnPrepareResponse = ctx =>
|
||||
@@ -116,6 +123,16 @@ namespace TerribleDev.Blog.Web
|
||||
"public,max-age=" + cacheTime;
|
||||
}
|
||||
});
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
|
||||
}
|
||||
|
||||
app.UseRewriter(new Microsoft.AspNetCore.Rewrite.RewriteOptions().AddRedirect("(.*[^/|.xml|.html])$", "$1/", 301));
|
||||
app.UseIENoOpen();
|
||||
app.UseNoMimeSniff();
|
||||
@@ -138,10 +155,6 @@ namespace TerribleDev.Blog.Web
|
||||
// },
|
||||
UpgradeInsecureRequests = true
|
||||
});
|
||||
if(Env.IsProduction())
|
||||
{
|
||||
app.UseOutputCaching();
|
||||
}
|
||||
app.UseWebMarkupMin();
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(endpoints =>
|
||||
@@ -150,4 +163,4 @@ namespace TerribleDev.Blog.Web
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Taghelpers
|
||||
{
|
||||
public abstract class AbstractPlatformTagHelper : TagHelper
|
||||
{
|
||||
static Regex MobileCheck = new Regex(@"(?:phone|windows\s+phone|ipod|blackberry|(?:android|bb\d+|meego|silk|googlebot) .+? mobile|palm|windows\s+ce|opera\ mini|avantgo|mobilesafari|docomo|ipad)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript);
|
||||
static ConcurrentDictionary<string, Platform> CachedChecks = new ConcurrentDictionary<string, Platform>(); // dictionary of user agent -> mobilre
|
||||
protected HttpRequest Request => ViewContext.HttpContext.Request;
|
||||
protected HttpResponse Response => ViewContext.HttpContext.Response;
|
||||
|
||||
[ViewContext]
|
||||
public ViewContext ViewContext { get; set; }
|
||||
protected abstract bool ShouldRender();
|
||||
public Platform GetPlatform()
|
||||
{
|
||||
var userAgent = this.Request.Headers.UserAgent;
|
||||
if (string.IsNullOrEmpty(userAgent))
|
||||
{
|
||||
return Platform.Desktop; // mobile is default
|
||||
}
|
||||
if(CachedChecks.TryGetValue(userAgent, out var cacheResult))
|
||||
{
|
||||
return cacheResult;
|
||||
}
|
||||
var isMobile = AbstractPlatformTagHelper.MobileCheck.IsMatch(this.Request.Headers.UserAgent);
|
||||
return isMobile ? Platform.Mobile : Platform.Desktop;
|
||||
}
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
output.TagName = null;
|
||||
if(!this.ShouldRender())
|
||||
{
|
||||
output.SuppressOutput();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/TerribleDev.Blog.Web/Taghelpers/Desktop.cs
Normal file
16
src/TerribleDev.Blog.Web/Taghelpers/Desktop.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
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("desktop", TagStructure = TagStructure.NormalOrSelfClosing)]
|
||||
public class DesktopTagHelper : AbstractPlatformTagHelper
|
||||
{
|
||||
protected override bool ShouldRender() => this.GetPlatform() == Platform.Desktop;
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,13 @@ using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Taghelpers
|
||||
{
|
||||
[HtmlTargetElement("link", Attributes = "rel, href, http-2-push")]
|
||||
public record PushUrl(string Url, string asProperty);
|
||||
[HtmlTargetElement("link", Attributes = "[rel=stylesheet],href,push")]
|
||||
[HtmlTargetElement("img", Attributes = "src,push")]
|
||||
[HtmlTargetElement("script", Attributes = "src,push")]
|
||||
public class HttpPush : LinkTagHelper
|
||||
{
|
||||
[HtmlAttributeNotBound]
|
||||
public bool Http2PushEnabled { get; set; } = true;
|
||||
|
||||
public static readonly string Key = "http2push-link";
|
||||
@@ -24,23 +28,35 @@ namespace TerribleDev.Blog.Web.Taghelpers
|
||||
{
|
||||
}
|
||||
|
||||
private (string Url, string AsProperty) GetTagInfo(string tag) =>
|
||||
tag switch
|
||||
{
|
||||
"link" => ("href", "link"),
|
||||
"img" => ("src", "image"),
|
||||
"script" => ("src", "script"),
|
||||
_ => (null, null)
|
||||
};
|
||||
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
if(!this.Http2PushEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var url = base.TryResolveUrl(output.Attributes["href"].Value.ToString(), out string resolvedUrl) ? resolvedUrl : output.Attributes["href"].Value.ToString();
|
||||
var linkList = ViewContext.HttpContext.Items.TryGetValue(Key, out var links) ? links as List<string> : null;
|
||||
var (urlAttribute, asProperty) = GetTagInfo(output.TagName);
|
||||
var url = base.TryResolveUrl(output.Attributes[urlAttribute].Value.ToString(), out string resolvedUrl) ? resolvedUrl : output.Attributes[urlAttribute].Value.ToString();
|
||||
var linkList = ViewContext.HttpContext.Items.TryGetValue(Key, out var links) ? links as List<PushUrl> : null;
|
||||
|
||||
if(linkList == null)
|
||||
{
|
||||
linkList = new List<string>() { url };
|
||||
linkList = new List<PushUrl>() { new PushUrl(url, asProperty) };
|
||||
ViewContext.HttpContext.Items.Add(HttpPush.Key, linkList);
|
||||
}
|
||||
else
|
||||
{
|
||||
linkList.Add(url);
|
||||
linkList.Add(new PushUrl(url, asProperty));
|
||||
}
|
||||
output.Attributes.Remove(output.Attributes["push"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,34 +8,9 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Taghelpers
|
||||
{
|
||||
[HtmlTargetElement("desktopOnly", TagStructure = TagStructure.NormalOrSelfClosing)]
|
||||
public class DesktopTagHelper : TagHelper
|
||||
[HtmlTargetElement("mobile", TagStructure = TagStructure.NormalOrSelfClosing)]
|
||||
public class MobileTagHelper : AbstractPlatformTagHelper
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
protected override bool ShouldRender() => this.GetPlatform() == Platform.Mobile;
|
||||
}
|
||||
}
|
||||
|
||||
8
src/TerribleDev.Blog.Web/Taghelpers/Platforms.cs
Normal file
8
src/TerribleDev.Blog.Web/Taghelpers/Platforms.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace TerribleDev.Blog.Web.Taghelpers
|
||||
{
|
||||
public enum Platform
|
||||
{
|
||||
Desktop,
|
||||
Mobile,
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<RuntimeIdentifiers>linux-musl-x64</RuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -22,12 +23,11 @@
|
||||
<PackageReference Include="Markdig" Version="0.15.7" />
|
||||
<PackageReference Include="Schema.NET" Version="11.0.1" />
|
||||
<PackageReference Include="UriBuilder.Fluent" Version="1.5.2" />
|
||||
<PackageReference Include="WebMarkupMin.AspNetCore6" Version="2.11.0" />
|
||||
<PackageReference Include="WebMarkupMin.AspNetCore7" Version="2.13.0-rc1" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.3.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" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.1" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.0" Condition="'$(Configuration)' == 'Debug'" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
@model PostViewModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Post.Title;
|
||||
var amp = ViewData["amp"] as bool? ?? false;
|
||||
}
|
||||
|
||||
<cache vary-by-route="postUrl,amp">
|
||||
@@ -24,10 +23,7 @@
|
||||
<meta name="twitter:site" content="@@TerribleDev">
|
||||
<meta name="twitter:creator" content="@@TerribleDev">
|
||||
<link rel="canonical" href="@Model.Post.CanonicalUrl" />
|
||||
@if(Model.Post.isAmp)
|
||||
{
|
||||
<link rel="amphtml" href="@Model.Post.AMPUrl">
|
||||
}
|
||||
|
||||
@if(!string.IsNullOrEmpty(Model.Post.ThumbnailImage))
|
||||
{
|
||||
<meta name="twitter:image" content="@(Model.Post.ThumbnailImage)">
|
||||
|
||||
@@ -5,13 +5,6 @@
|
||||
<article>
|
||||
<h1 itemprop="headline" class="headline">@Model.Title</h1>
|
||||
<time class="headlineSubtext" itemprop="datePublished" content="@Model.PublishDate.ToString()">@Model.PublishDate.ToString("D")</time>
|
||||
@if(amp)
|
||||
{
|
||||
@Model.Content.AmpContent
|
||||
}
|
||||
else
|
||||
{
|
||||
@Model.Content.Content
|
||||
}
|
||||
@Model.Content.Content
|
||||
|
||||
</article>
|
||||
@@ -1,18 +1,9 @@
|
||||
@model Post
|
||||
@{
|
||||
var amp = ViewData["amp"] as bool? ?? false;
|
||||
}
|
||||
|
||||
<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>
|
||||
@if(amp)
|
||||
{
|
||||
@Model.Content.AmpContent
|
||||
}
|
||||
else
|
||||
{
|
||||
@Model.Content.Content
|
||||
}
|
||||
@Model.Content.Content
|
||||
@if (Model.tags.Count > 0)
|
||||
{
|
||||
<div>
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
var amp = ViewData["amp"] as bool? ?? false;
|
||||
}
|
||||
@if(!amp)
|
||||
{
|
||||
<script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() { dataLayer.push(arguments); }
|
||||
gtag('js', new Date());
|
||||
@@ -18,20 +11,4 @@
|
||||
document.body.appendChild(script);
|
||||
}, 4000)
|
||||
});
|
||||
</script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<amp-analytics type="gtag" data-credentials="include">
|
||||
<script type="application/json">
|
||||
{
|
||||
"vars" : {
|
||||
"gtag_id": "UA-48128396-1",
|
||||
"config" : {
|
||||
"UA-48128396-1": { "UA-48128396-1": "default" }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</amp-analytics>
|
||||
}
|
||||
</script>
|
||||
@@ -1,20 +1,10 @@
|
||||
@{
|
||||
var amp = ViewData["amp"] as bool?;
|
||||
}
|
||||
|
||||
|
||||
<nav class="navBar hide" id="navBar">
|
||||
<div class="navContent">
|
||||
@if(amp == true)
|
||||
{
|
||||
<img src="/content/tommyAvatar4.jpg" loading="lazy" alt="An image of TerribleDev" class="round navHero" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<picture class="navHero">
|
||||
<picture class="navHero">
|
||||
<source srcset="/content/tommyAvatar4.jpg.webp" loading="lazy" type="image/webp" alt="An image of TerribleDev" class="round" />
|
||||
<img src="/content/tommyAvatar4.jpg" loading="lazy" alt="An image of TerribleDev" class="round" />
|
||||
</picture>
|
||||
}
|
||||
</picture>
|
||||
<span>Tommy "Terrible Dev" Parnell</span>
|
||||
<ul class="sidebarBtns">
|
||||
<li><a href="/" class="link-unstyled">Home</a></li>
|
||||
@@ -24,7 +14,7 @@
|
||||
<li><a href="https://github.com/terribledev" rel="noopener" target="_blank" class="link-unstyled">Github</a></li>
|
||||
<li><a href="https://twitter.com/terribledev" rel="noopener" target="_blank" class="link-unstyled">Twitter</a></li>
|
||||
<li><a href="mailto:tommy@terribledev.io" class="link-unstyled">Email</a></li>
|
||||
<li><span class="link-unstyled" id="closeNav">Close Navbar</span></li>
|
||||
<li><span onclick="toggleNav()" class="link-unstyled" id="closeNav">Close Navbar</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,62 +1,48 @@
|
||||
@inject BlogConfiguration config
|
||||
|
||||
@{
|
||||
var amp = ViewData["amp"] as bool? ?? false;
|
||||
var htmlTag = amp ? "amp" : "";
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" @htmlTag>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="author" content="Tommy "TerribleDev" Parnell" />
|
||||
|
||||
<title>@ViewData["Title"] | @config.Title</title>
|
||||
<environment names="Development">
|
||||
<inline-style href="css/site.css,css/site.mobile.css,css/site.desktop.css"></inline-style>
|
||||
@* <desktop>
|
||||
<inline-style href="css/site.css,css/site.desktop.css"></inline-style>
|
||||
</desktop>
|
||||
<mobile>
|
||||
<inline-style href="css/site.css,css/site.mobile.css"></inline-style>
|
||||
</mobile> *@
|
||||
</environment>
|
||||
<environment names="Production">
|
||||
@* <desktop>
|
||||
<inline-style href="css/site.min.css,css/site.desktop.min.css"></inline-style>
|
||||
</desktop>
|
||||
<mobile>
|
||||
<inline-style href="css/site.min.css,css/site.mobile.min.css"></inline-style>
|
||||
</mobile> *@
|
||||
<inline-style href="css/site.min.css,css/site.mobile.min.css,css/site.desktop.min.css"></inline-style>
|
||||
</environment>
|
||||
<environment names="Production">
|
||||
<partial name="Gtm" />
|
||||
</environment>
|
||||
|
||||
|
||||
<meta name="theme-color" content="#4A4A4A" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="alternate" type="application/atom+xml" title="RSS" href="/rss.xml">
|
||||
@if(!amp)
|
||||
{
|
||||
<environment names="Production">
|
||||
<partial name="Gtm" />
|
||||
</environment>
|
||||
}
|
||||
<link rel="manifest" href="~/manifest.json" asp-append-version="true">
|
||||
<link asp-append-version="true" rel="icon" href="~/favicon.ico" />
|
||||
<title>@ViewData["Title"] | @config.Title</title>
|
||||
@if(amp)
|
||||
{
|
||||
<inline-style amp-custom href="css/site.css,css/site.mobile.css,css/site.desktop.css"></inline-style>
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
<environment names="Development">
|
||||
<inline-style href="css/site.css,css/site.mobile.css,css/site.desktop.css"></inline-style>
|
||||
</environment>
|
||||
<environment names="Production">
|
||||
<inline-style href="css/site.min.css,css/site.mobile.min.css,css/site.desktop.min.css"></inline-style>
|
||||
</environment>
|
||||
}
|
||||
|
||||
|
||||
@if(amp)
|
||||
{
|
||||
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
|
||||
<script async src="https://cdn.ampproject.org/v0.js"></script>
|
||||
<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
|
||||
}
|
||||
<meta name="author" content="Tommy "TerribleDev" Parnell" />
|
||||
<link asp-append-version="true" rel="icon" href="~/favicon.ico" push />
|
||||
@RenderSection("Head", false)
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-main" href="#main">Skip to main content</a>
|
||||
<div class="rootbox">
|
||||
<header class="header">
|
||||
@if(amp)
|
||||
{
|
||||
<a class="btn" id="menuBtn" href="/"> back to home </a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<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>
|
||||
}
|
||||
<svg aria-label="Open Menu" onclick="toggleNav()" 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 ">@config.Title</a></div>
|
||||
</header>
|
||||
<partial name="Nav" />
|
||||
@@ -65,21 +51,12 @@
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@if(!amp)
|
||||
{
|
||||
@RenderSection("Scripts", required: false)
|
||||
<environment names="Development">
|
||||
<script asp-append-version="true" src="~/js/swi.js" async></script>
|
||||
<script push 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>
|
||||
<script push asp-append-version="true" src="~/js/site.min.js" async></script>
|
||||
</environment>
|
||||
}
|
||||
else
|
||||
{
|
||||
<environment names="Production">
|
||||
<partial name="Gtm" />
|
||||
</environment>
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
src/TerribleDev.Blog.Web/wwwroot/css/dark.old.css
Normal file
13
src/TerribleDev.Blog.Web/wwwroot/css/dark.old.css
Normal file
@@ -0,0 +1,13 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--headline: #f0f0f0;
|
||||
--body-text-color: #ffffff;
|
||||
--block-quote-left-border: #d1dced;
|
||||
--code-block-background-color: #4a4a4a;
|
||||
--primary-background: #323131;
|
||||
--link-color: #3faff9;
|
||||
/* --link-visited: #d8dbde; */
|
||||
--border-color: #bdcad2;
|
||||
--horizontal-rule: #626468;
|
||||
}
|
||||
}
|
||||
@@ -13,20 +13,6 @@
|
||||
--nav-bar-text-color: var(--primary-background);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--headline: #f0f0f0;
|
||||
--body-text-color: #ffffff;
|
||||
--block-quote-left-border: #d1dced;
|
||||
--code-block-background-color: #4a4a4a;
|
||||
--primary-background: #323131;
|
||||
--link-color: #3faff9;
|
||||
/* --link-visited: #d8dbde; */
|
||||
--border-color: #bdcad2;
|
||||
--horizontal-rule: #626468;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.9 MiB |
@@ -3,7 +3,9 @@
|
||||
//Install stage sets up the offline page in the cache and opens a new cache
|
||||
|
||||
self.addEventListener('install', function (event) {
|
||||
event.waitUntil(preLoad());
|
||||
setTimeout(function() {
|
||||
event.waitUntil(preLoad());
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
var preLoad = function () {
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
//Add this below content to your HTML page, or add the js file to your page at the very top to register sercie worker
|
||||
if (navigator && navigator.serviceWorker && navigator.serviceWorker.controller) {
|
||||
console.log('[PWA Builder] active service worker found, no need to register')
|
||||
} else if (navigator && navigator.serviceWorker) {
|
||||
|
||||
//Register the ServiceWorker
|
||||
navigator.serviceWorker.register('/sw.min.js', {
|
||||
scope: '/'
|
||||
}).then(function (reg) {
|
||||
console.log('Service worker has been registered for scope:' + reg.scope);
|
||||
}).then(() => {
|
||||
console.log('SW');
|
||||
});
|
||||
}
|
||||
|
||||
var toggleNav = function () {
|
||||
function toggleNav () {
|
||||
var nav = document.getElementById('navBar');
|
||||
if (!nav) {
|
||||
return;
|
||||
@@ -23,15 +22,4 @@ var toggleNav = function () {
|
||||
else {
|
||||
nav.classList.add('hide');
|
||||
}
|
||||
}
|
||||
function attachNavToggle(elementId) {
|
||||
var menuButton = document.getElementById(elementId);
|
||||
if (menuButton) {
|
||||
menuButton.addEventListener('click', function () {
|
||||
toggleNav();
|
||||
});
|
||||
}
|
||||
}
|
||||
attachNavToggle('menuBtn');
|
||||
attachNavToggle('closeNav');
|
||||
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Allow: /
|
||||
Sitemap: https://blog.terrible.dev/sitemap.xml
|
||||
Reference in New Issue
Block a user