Finally updated

This took way too long
This commit is contained in:
Toastie 2025-01-22 01:36:44 +13:00
parent 95ff7584f1
commit 69db7ab825
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
70 changed files with 5963 additions and 6210 deletions

View file

@ -1,6 +0,0 @@
.husky
.vscode
node_modules
public
dist
.yarn

View file

@ -1,23 +0,0 @@
module.exports = {
env: {
node: true,
es2022: true,
browser: true,
},
extends: ["eslint:recommended", "plugin:astro/recommended"],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
overrides: [
{
files: ["*.astro"],
parser: "astro-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
extraFileExtensions: [".astro"],
},
rules: {},
},
],
};

3
.gitignore vendored
View file

@ -22,9 +22,6 @@ pnpm-debug.log*
# ignore .astro directory
.astro
# ignore Jampack cache files
.jampack/
# yarn
.yarn/*
!.yarn/patches

View file

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

View file

@ -9,5 +9,5 @@
!astro.config.ts
!package.json
!.prettierrc
!.eslintrc.js
!eslint.config.mjs
!README.md

34
.vscode/blog.code-snippets vendored Normal file
View file

@ -0,0 +1,34 @@
{
"Frontmatter": {
"scope": "markdown",
"prefix": "frontmatter",
"body": [
"---",
"author: $1",
"pubDatetime: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}T$CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND.000$CURRENT_TIMEZONE_OFFSET",
"modDatetime: $3",
"title: $4",
"featured: ${5|false,true|}",
"draft: ${6|true,false|}",
"tags:",
" - $7",
"description: $8",
"---",
],
"description": "Adds the frontmatter block for the Blog post"
},
"Blog Template": {
"scope": "markdown",
"prefix": "template",
"body": [
"${1:frontmatter}",
"",
"${2: Introductory Sentence}",
"",
"## Table of contents",
"",
"## ${3: heading 1}",
],
"description": "Adds the template for automating the creation of a Blog post. You will need to trigger the snippet modal on the 'frontmatter' line to insert the other snippet."
}
}

View file

@ -1,3 +1,3 @@
# Toastie's blog
The code responsible for making the blog at https://blog.toastiet0ast.com work
The code responsible for making the blog at https://blog.elliebot.net work

View file

@ -14,7 +14,9 @@ export default defineConfig({
applyBaseStyles: false,
}),
react(),
sitemap(),
sitemap({
filter: page => SITE.showArchives || !page.endsWith("/archives"),
}),
],
markdown: {
remarkPlugins: [
@ -27,7 +29,8 @@ export default defineConfig({
],
],
shikiConfig: {
theme: "one-dark-pro",
// For more themes, visit https://shiki.style/themes
themes: { light: "min-light", dark: "night-owl" },
wrap: true,
},
},
@ -37,4 +40,7 @@ export default defineConfig({
},
},
scopedStyleStrategy: "where",
experimental: {
contentLayer: true,
},
});

44
eslint.config.mjs Normal file
View file

@ -0,0 +1,44 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import astroParser from "astro-eslint-parser";
import eslintPluginAstro from "eslint-plugin-astro";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
...eslintPluginAstro.configs.recommended,
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{
files: ["*.astro"],
languageOptions: {
parser: astroParser,
parserOptions: {
parser: "@typescript-eslint/parser",
extraFileExtensions: [".astro"],
},
},
},
{
files: ["tailwind.config.cjs", "**/*.d.ts"],
rules: {
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/triple-slash-reference": "off",
},
},
{
rules: {
"@typescript-eslint/no-unused-expressions": "off",
},
},
{
ignores: ["dist/**", ".astro"],
},
];

View file

@ -1,63 +1,49 @@
{
"name": "toasite-blog",
"version": "3.0.0",
"private": false,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build && jampack ./dist",
"build": "astro build",
"preview": "astro preview",
"sync": "astro sync",
"astro": "astro",
"format:check": "prettier --plugin-search-dir=. --check .",
"format": "prettier --plugin-search-dir=. --write .",
"cz": "cz",
"prepare": "husky install",
"format:check": "prettier --check . --plugin=prettier-plugin-astro",
"format": "prettier --write . --plugin=prettier-plugin-astro",
"lint": "eslint ."
},
"dependencies": {
"@astrojs/check": "^0.2.0",
"@astrojs/rss": "^3.0.0",
"@resvg/resvg-js": "^2.4.1",
"astro": "^3.1.3",
"fuse.js": "^6.6.2",
"github-slugger": "^2.0.0",
"@astrojs/check": "^0.9.3",
"@astrojs/rss": "^4.0.7",
"@resvg/resvg-js": "^2.6.2",
"astro": "^4.16.18",
"fuse.js": "^7.0.0",
"lodash.kebabcase": "^4.1.1",
"remark-collapse": "^0.1.2",
"remark-toc": "^9.0.0",
"satori": "^0.10.8",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2"
"satori": "^0.11.0",
"tailwindcss": "^3.4.11",
"typescript": "^5.5.3"
},
"devDependencies": {
"@astrojs/react": "^3.0.2",
"@astrojs/sitemap": "^3.0.0",
"@astrojs/tailwind": "^5.0.0",
"@divriots/jampack": "^0.20.2",
"@tailwindcss/typography": "^0.5.10",
"@astrojs/react": "^3.6.2",
"@astrojs/sitemap": "^3.1.6",
"@astrojs/tailwind": "^5.1.0",
"@tailwindcss/typography": "^0.5.15",
"@types/github-slugger": "^1.3.0",
"@types/react": "^18.2.22",
"@typescript-eslint/parser": "^6.7.3",
"astro-eslint-parser": "^0.15.0",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.50.0",
"eslint-plugin-astro": "^0.29.0",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"prettier": "^3.0.3",
"prettier-plugin-astro": "^0.12.0",
"prettier-plugin-tailwindcss": "^0.5.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
"@types/lodash.kebabcase": "^4.1.9",
"@types/react": "^18.3.6",
"@typescript-eslint/parser": "^8.5.0",
"astro-eslint-parser": "^1.0.3",
"eslint": "^9.10.0",
"eslint-plugin-astro": "^1.2.4",
"globals": "^15.9.0",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript-eslint": "^8.5.0"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx,md,mdx,json}": [
"prettier --plugin-search-dir=. --write"
]
},
"packageManager": "pnpm@8.13.1+sha1.90f9b2bb3ed58632bcb7b13c3902d6873ee9501c"
}

File diff suppressed because it is too large Load diff

View file

@ -1,361 +0,0 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" width="865.76" height="682.89" viewBox="0 0 865.76 682.89">
<defs>
<style xmlns="http://www.w3.org/1999/xhtml">*, body, html { -webkit-font-smoothing: antialiased; }
img, svg { max-width: 100%; }
</style>
</defs>
<path d="M391.82,532.2c-15.44,2.82-87.85,18.09-73.28,55a33.24,33.24,0,0,0,9.74,13.13c18.18,15.25,83.33,52.58,272.06,32.22,10.69-1.15,21.42-1.86,32.17-2.06,49.73-.92,206-9.34,202-78.54,0,0-2.07-38.74-95.7-26.87l-71.21-4.43s-160.55-12.38-268.7,10.11C396.57,531.29,394.2,531.77,391.82,532.2Z" fill="#787878" data-primary="true"/>
<path d="M391.82,532.2c-15.44,2.82-87.85,18.09-73.28,55a33.24,33.24,0,0,0,9.74,13.13c18.18,15.25,83.33,52.58,272.06,32.22,10.69-1.15,21.42-1.86,32.17-2.06,49.73-.92,206-9.34,202-78.54,0,0-2.07-38.74-95.7-26.87l-71.21-4.43s-160.55-12.38-268.7,10.11C396.57,531.29,394.2,531.77,391.82,532.2Z" fill="#fff" opacity="0.7"/>
<path d="M503.08,522.3C179.26,552.79,133.91,359.63,133.91,359.63c-24.79-67.13-3.45-111,27.66-152.68a303.36,303.36,0,0,1,117.77-94.5c74.9-34.06,126.36-41,126.36-41S622.58,15.68,735.64,183.7c0,0,108,135.55,37.54,221.14,0,0-35.14,47.34-127.63,82.89l-69.3,20.46A387.7,387.7,0,0,1,503.08,522.3Z" fill="#787878" data-primary="true"/>
<path d="M503.08,522.3C179.26,552.79,133.91,359.63,133.91,359.63c-24.79-67.13-3.45-111,27.66-152.68a303.36,303.36,0,0,1,117.77-94.5c74.9-34.06,126.36-41,126.36-41S622.58,15.68,735.64,183.7c0,0,108,135.55,37.54,221.14,0,0-35.14,47.34-127.63,82.89l-69.3,20.46A387.7,387.7,0,0,1,503.08,522.3Z" fill="#fff" opacity="0.7"/>
<rect x="104.67" y="206.46" width="463.2" height="348.88" fill="#fff"/>
<rect x="108.43" y="206.46" width="459.44" height="35.42" fill="#e6e6e6"/>
<rect x="128.82" y="259.06" width="104.13" height="104.13" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="713.86" y="369.62" width="5.37" height="37.57" fill="#999"/>
<polygon points="664.89 442.18 664.89 554.44 672.53 554.44 676.93 436.58 664.89 442.18" fill="#ccc"/>
<polygon points="711.71 420.08 711.71 537.08 719.36 537.08 723.52 414.71 711.71 420.08" fill="#ccc"/>
<polygon points="668.23 434.1 733.18 405.05 703.86 399.96 670.01 385.44 668.23 434.1" fill="#ccc"/>
<path d="M656.14,446.25l77-35.83v-5.37L668.23,434.1S660.68,442.36,656.14,446.25Z" fill="#b3b3b3"/>
<path d="M693.46,271.94H734a4.55,4.55,0,0,1,4.55,4.55v67.37a0,0,0,0,1,0,0H693.46a0,0,0,0,1,0,0V271.94A0,0,0,0,1,693.46,271.94Z" fill="#999"/>
<rect x="241.54" y="44.36" width="325.8" height="139.55" fill="#787878" data-primary="true"/>
<rect x="263.01" y="83.01" width="100.91" height="65.48" fill="#fff" opacity="0.3"/>
<g opacity="0.3">
<path d="M297.36,131.59a1.07,1.07,0,0,1-.76-.32l-14.79-14.76a1.08,1.08,0,0,1,0-1.5l14.79-15.56a1.07,1.07,0,0,1,1.56,1.47l-14.07,14.81,14.05,14a1.07,1.07,0,0,1,0,1.52A1.09,1.09,0,0,1,297.36,131.59Z" fill="#fff"/>
</g>
<g opacity="0.3">
<path d="M328.73,132.66a1.06,1.06,0,0,1-.76-.31,1.07,1.07,0,0,1,0-1.52l14-14L328,102a1.08,1.08,0,1,1,1.56-1.48l14.78,15.56a1.06,1.06,0,0,1,0,1.5l-14.78,14.77A1.07,1.07,0,0,1,328.73,132.66Z" fill="#fff"/>
</g>
<g opacity="0.3">
<path d="M305.56,131.59a1.08,1.08,0,0,1-1-1.56l14.34-28.18a1.08,1.08,0,1,1,1.92,1L306.51,131A1.07,1.07,0,0,1,305.56,131.59Z" fill="#fff"/>
</g>
<path d="M524.39,119.51H454.62a1.08,1.08,0,0,1,0-2.15h69.77a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
<path d="M540.5,132.39H454.62a1.08,1.08,0,0,1,0-2.15H540.5a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
<rect x="460.52" y="153.86" width="65.48" height="16.1" rx="7.5" fill="#fff" opacity="0.3"/>
<path d="M567.33,44.36V183.91H241.54s54.75-59.1,144.51-74c4.1-.68,8.24-1.12,12.38-1.4C426.41,106.6,557.79,95.18,567.33,44.36Z" fill="#fff" opacity="0.3"/>
<rect x="31.14" y="128.09" width="187.86" height="213.62" fill="#787878" data-primary="true"/>
<rect x="31.14" y="128.09" width="187.86" height="34.35" fill="#fff" opacity="0.3"/>
<rect x="46.17" y="173.18" width="57.97" height="57.97" fill="#282728" data-secondary="true"/>
<circle cx="164.78" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
<circle cx="184.11" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
<circle cx="203.43" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
<path d="M170.69,192.5H117a1.07,1.07,0,1,1,0-2.14h53.67a1.07,1.07,0,0,1,0,2.14Z" fill="#fff"/>
<path d="M186.25,205.38h-68.7a1.07,1.07,0,0,1,0-2.14h68.7a1.07,1.07,0,1,1,0,2.14Z" fill="#fff"/>
<path d="M203.43,218.27H117.55a1.08,1.08,0,0,1,0-2.15h85.88a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
<path d="M168,287H84.28a1.08,1.08,0,1,1,0-2.15H168a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
<path d="M194.84,299.85H57.44a1.08,1.08,0,1,1,0-2.15h137.4a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
<path d="M168.54,312.73H83.74a1.08,1.08,0,1,1,0-2.15h84.8a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
<rect x="83.74" y="248.32" width="78.36" height="16.1" fill="#fff" opacity="0.3"/>
<rect x="256.57" y="259.06" width="66.55" height="17.18" fill="#787878" opacity="0.29" data-primary="true"/>
<path d="M308.78,293.79H256.57a1.08,1.08,0,1,1,0-2.15h52.21a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M325.8,306.67H256.57a1.07,1.07,0,1,1,0-2.14H325.8a1.07,1.07,0,1,1,0,2.14Z" fill="#e6e6e6"/>
<path d="M339.76,319.55H256.57a1.07,1.07,0,1,1,0-2.14h83.19a1.07,1.07,0,0,1,0,2.14Z" fill="#e6e6e6"/>
<path d="M379.48,332.44H256.57a1.08,1.08,0,1,1,0-2.15H379.48a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="256.57" y="348.15" width="154.58" height="15.03" fill="#787878" opacity="0.29" data-primary="true"/>
<path d="M252.45,400.29h-122a1.08,1.08,0,0,1,0-2.15h122a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M353.18,400.29H268.91a1.08,1.08,0,0,1,0-2.15h84.27a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M417.59,400.29H388.06a1.08,1.08,0,0,1,0-2.15h29.53a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="256.57" y="396.53" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="360.69" y="396.53" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="373.57" y="396.53" width="5.37" height="5.37" fill="#ccc"/>
<path d="M223.29,429.16H131a1.08,1.08,0,0,1,0-2.15h92.32a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M289.84,455.37H129.9a1.08,1.08,0,1,1,0-2.15H289.84a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M325.27,429.16H255a1.08,1.08,0,1,1,0-2.15h70.31a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M349.42,455.37h-36a1.08,1.08,0,0,1,0-2.15h36a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="227.58" y="425.4" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="240.46" y="425.4" width="5.37" height="5.37" fill="#ccc"/>
<rect x="290.92" y="451.61" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="303.8" y="451.61" width="5.37" height="5.37" fill="#ccc"/>
<path d="M355.32,512.93H298.43a1.08,1.08,0,0,1,0-2.15h56.89a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M416,512.93H388.06a1.08,1.08,0,0,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="361.77" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
<rect x="374.65" y="509.17" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<path d="M416,455.37H375.72a1.08,1.08,0,0,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="353.18" y="451.61" width="5.37" height="5.37" fill="#ccc"/>
<rect x="366.06" y="451.61" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<path d="M205,485H131a1.08,1.08,0,0,1,0-2.15H205a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M349.42,485h-52.6a1.08,1.08,0,0,1,0-2.15h52.6a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M416,485H363.38a1.08,1.08,0,1,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="207.19" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
<rect x="220.07" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="231.88" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
<path d="M256.57,512.93H131a1.08,1.08,0,0,1,0-2.15h125.6a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="258.71" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
<rect x="271.59" y="509.17" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="283.4" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
<rect x="244.76" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="259.79" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
<rect x="271.59" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="284.48" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
<path d="M417.59,429.16H358a1.08,1.08,0,1,1,0-2.15h59.58a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="330.63" y="425.4" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="343.52" y="425.4" width="5.37" height="5.37" fill="#ccc"/>
<rect x="51.53" y="436.18" width="103.05" height="64.41" fill="#787878" data-primary="true"/>
<g opacity="0.3">
<path d="M88.5,485.36a1.06,1.06,0,0,1-.74-.3l-15.5-14.83a1.06,1.06,0,0,1,0-1.54l15.49-15a1.07,1.07,0,0,1,1.52,0,1.08,1.08,0,0,1,0,1.52l-14.7,14.25,14.69,14.06a1.07,1.07,0,0,1,0,1.52A1.1,1.1,0,0,1,88.5,485.36Z" fill="#fff"/>
</g>
<g opacity="0.3">
<path d="M119.16,485.36a1.07,1.07,0,0,1-.74-1.84l14.69-14.26L118.42,455.2a1.07,1.07,0,0,1,1.48-1.55l15.5,14.83a1.07,1.07,0,0,1,.33.77,1.08,1.08,0,0,1-.32.78l-15.5,15A1.08,1.08,0,0,1,119.16,485.36Z" fill="#fff"/>
</g>
<g opacity="0.3">
<path d="M96.62,483.41a1.11,1.11,0,0,1-.5-.12,1.07,1.07,0,0,1-.45-1.45l14-26.83a1.08,1.08,0,1,1,1.91,1l-14,26.83A1.06,1.06,0,0,1,96.62,483.41Z" fill="#fff"/>
</g>
<rect x="434.76" y="367.48" width="11.81" height="208.25" fill="#999"/>
<rect x="441.2" y="367.48" width="5.37" height="208.25" opacity="0.1"/>
<rect x="471.26" y="368.01" width="11.81" height="172.29" fill="#999"/>
<rect x="477.7" y="368.01" width="5.37" height="172.29" opacity="0.1"/>
<rect x="728.89" y="367.48" width="11.81" height="208.25" fill="#999"/>
<rect x="735.33" y="367.48" width="5.37" height="208.25" opacity="0.1"/>
<rect x="758.95" y="354.06" width="11.81" height="186.25" fill="#999"/>
<rect x="765.39" y="354.06" width="5.37" height="186.25" opacity="0.1"/>
<path d="M688.1,271.94h40.53a4.55,4.55,0,0,1,4.55,4.55v67.37a0,0,0,0,1,0,0H688.1a0,0,0,0,1,0,0V271.94A0,0,0,0,1,688.1,271.94Z" fill="#b3b3b3"/>
<polygon points="421.88 364.26 477.27 336.37 786.88 336.37 750.36 364.26 421.88 364.26" fill="#ccc"/>
<path d="M542.11,559.63l-32.5,25.42S496,597.2,507.76,604.71c0,0,17.17,10.74,31.13-7.51l19.37-31.64Z" fill="#787878" data-primary="true"/>
<path d="M505.61,596.12c8,8.68,20.58,6.87,28.45-1,3.7-3.79,7-8.33,10.52-12.3,3.08-3.62,7.51-8.79,10.65-12.28-2.8,3.74-7.06,9.09-10,12.81-3.41,4.12-6.73,8.65-10.42,12.54-8.21,8.11-21.45,9.88-29.19.26Z" opacity="0.2"/>
<path d="M512.32,583.74c6.45-.09,13.31,2.42,17.35,7.63a15.61,15.61,0,0,1,2.79,5.84c-.26-.47-.51-1-.74-1.43a8.51,8.51,0,0,0-.81-1.37c-4-6.39-11.44-9.4-18.59-10.67Z" opacity="0.2"/>
<path d="M519.56,580c4.83-.65,11.72.93,12.9,6.4-2.62-4.61-8.1-5.41-12.9-6.4Z" opacity="0.2"/>
<path d="M523.86,575.73c4.82-.65,11.72.93,12.89,6.39-2.61-4.6-8.1-5.4-12.89-6.39Z" opacity="0.2"/>
<path d="M532.45,569.29c4.82-.65,11.72.93,12.89,6.39-2.61-4.61-8.1-5.4-12.89-6.39Z" opacity="0.2"/>
<path d="M550.16,544.06l-8,15.57s-3.32,4,1.25,6.48a8.52,8.52,0,0,0,4.06,1h7.9a3.61,3.61,0,0,0,2.94-1.51L568,551.93S554.41,546.7,550.16,544.06Z" fill="#f9b499"/>
<polygon points="548.32 510.23 551.84 520.86 557.18 505.66 548.32 510.23" fill="#f9b499"/>
<path d="M710.77,332.4c0,8.24-5.5,8.24-5.5,8.24l-15,5.37c-6.68,2.23-9.44,1.89-10.5,1.46a1.62,1.62,0,0,0-1.36.1.24.24,0,0,1-.08.06,11.71,11.71,0,0,1-3.82,1.75h0c-2.89.58-2.48-2.31-2.48-2.31a12.77,12.77,0,0,0,.2-1.54,9.91,9.91,0,0,0-5.8-9.37,26.59,26.59,0,0,0-4.77-1.68,6.38,6.38,0,0,0-3.07,0l-8.33,1.91H608.39L570,335.27a98.24,98.24,0,0,1,12.58-65.48,86.2,86.2,0,0,1,8.82-12.47c.72-.84,1.14-1.27,1.14-1.27l2.37-3.43s32,2.14,32.37,1.07,22.15-14,22.15-14l30.76,13a10.63,10.63,0,0,1,3.59,2.59c6.62,6.85,11.81,23.17,11.81,23.17l14,46.16A30.89,30.89,0,0,1,710.77,332.4Z" fill="#787878" data-primary="true"/>
<path d="M675.8,305s-30.74,5-53.75.22c-.59-.12-1.17-.27-1.75-.43A88.92,88.92,0,0,0,592.56,302l-22.06-.18-1.09,7.3h36.87s12,.39,21.7,3.61c0,0,9.66,3.22,29,1.07l21.82-2.66Z" fill="#282728" data-secondary="true"/>
<path d="M683.8,255.21c-20.39,2.6-56.89,14.58-56.89,14.58-8.59-6.44-35.49-12.47-35.49-12.47.72-.84,1.14-1.27,1.14-1.27l2.37-3.43s32,2.14,32.37,1.07,22.15-14,22.15-14l30.76,13A10.63,10.63,0,0,1,683.8,255.21Z" opacity="0.2"/>
<path d="M620.1,254.32a12.38,12.38,0,0,1-1.24.26c-7.26,1.28-14.75-1.87-20.74-8a43,43,0,0,1-10.73-19.86c-4.59-18.58,2.63-36.33,16.12-39.66s28.13,9,32.72,27.59S633.6,251,620.1,254.32Z" fill="#f9b499"/>
<ellipse cx="639.26" cy="215.05" rx="1.61" ry="3.22" fill="none" stroke="red" stroke-miterlimit="10" stroke-width="0.75"/>
<path d="M651.6,210.75s17.18,9.45-10.73,18.14a15.44,15.44,0,0,1-.54-3.65,15.8,15.8,0,0,1,.54-4.54l-8.59-4.36s-4.67.85-7.17-4.52c0,0-8.93,0-7.86-6.44,0,0-6.44,4.3-8.59-1.07,0,0-9.69,6.88-19.33-4.62,0,0-4.28,7.84-3,21,0,0-6.34,5.08-9.93.38a6.63,6.63,0,0,1-1.28-3.77,5.58,5.58,0,0,1,3.22-5.49s-7.77-2.89-7.56-9.28a10.2,10.2,0,0,1,1.41-4.67s1.61-4,7.63-2.31h0a19.17,19.17,0,0,1,3.1,1.24s-8.21-17.26,3.4-28.49c0,0,19.14-19.82,26.66,4.87,0,0,6.55-10.14,17-7.62h0a15.76,15.76,0,0,1,2.25.72s7.51,2,6.44,14.1c0,0,9.66-8.28,18.25,1.38,0,0,6.44,7.89,0,16.28C657,198.05,660.19,205.38,651.6,210.75Z" fill="#282728" data-secondary="true"/>
<path d="M590.41,197.6s-3.22-10.46,6.44-16.91c0,0,6.93-4.51,16.49,1.46a23.89,23.89,0,0,1,2.73,2.07,16.44,16.44,0,0,0,10.59,4.11s11-.47,12.6,12c0,0-12.35-10.72-21.47-5.54,0,0-4.83-15.22-17.72-10.93C600.07,183.91,592.56,186.06,590.41,197.6Z" opacity="0.2"/>
<path d="M579.79,195.56c-5.23.93-9,7-9,7a10.2,10.2,0,0,1,1.41-4.67S573.77,193.84,579.79,195.56Z" opacity="0.2"/>
<path d="M651.6,210.75s17.18,9.45-10.73,18.14a7.4,7.4,0,0,1-.54-3.65,7.26,7.26,0,0,1,.54-2.07l4.29-6.83h0c3.22-1.93,3.22-6.66,3.22-6.66a10.45,10.45,0,0,0,4.63-3.63,8.74,8.74,0,0,0,1.09-8.24c-2.23-5.68-8.94-4.09-8.94-4.09,2.15-16-7.52-10.85-7.52-10.85-1-14.52-7.22-17.15-7.61-17.3a15.76,15.76,0,0,1,2.25.72s7.51,2,6.44,14.1c0,0,9.66-8.28,18.25,1.38,0,0,6.44,7.89,0,16.28C657,198.05,660.19,205.38,651.6,210.75Z" opacity="0.2"/>
<path d="M659.11,241.88l-26.83,22.77c-4.3,3.45-6.44.85-6.44.85l-1.19-1.87-5.79-9c20.4-4.65,18.67-31.41,18.67-31.41h3.34a17.63,17.63,0,0,0,2.14,11.2c3.91-1.57,9-1.42,11.4-1.23a14.8,14.8,0,0,1,2.89.49C665.93,236,659.11,241.88,659.11,241.88Z" fill="#f9b499"/>
<path d="M659.11,241.88l-26.83,22.77c-4.3,3.45-6.44.85-6.44.85l-1.19-1.87c11.69-5.09,23-18.83,23-18.83,4.06-6-2.64-9.53-4.26-10.28a.16.16,0,0,1,0-.29c3.86-1.42,8.68-1.27,11-1.09a14.8,14.8,0,0,1,2.89.49C665.93,236,659.11,241.88,659.11,241.88Z" fill="#f7a48b"/>
<path d="M618.86,254.58l3.07,4.81s18.66-10.53,15-26.36C637,233,635.5,251.18,618.86,254.58Z" fill="#f7a48b"/>
<path d="M599,253.69a55.57,55.57,0,0,1,18.79,6.51" fill="none" stroke="red" stroke-miterlimit="10" stroke-width="0.75"/>
<path d="M710.77,332.4c0,8.24-5.5,8.24-5.5,8.24l-15,5.37c-6.68,2.23-9.44,1.89-10.5,1.46a1.62,1.62,0,0,0-1.36.1c.36-.24,1.68-1.46-.08-5l-5.63-8.42a1.13,1.13,0,0,1,.39-1.6,1.07,1.07,0,0,1,.55-.14,1.12,1.12,0,0,1,.91.46l7.14,9.93s1.07,4.29,15-2.15C696.69,340.64,708.75,336,710.77,332.4Z" opacity="0.2"/>
<path d="M674.48,349.38h0c-2.89.58-2.48-2.31-2.48-2.31a12.77,12.77,0,0,0,.2-1.54,9.91,9.91,0,0,0-5.8-9.37,26.59,26.59,0,0,0-4.77-1.68s8.22-3.51,12.51,7.22C674.14,341.71,675.46,346.35,674.48,349.38Z" opacity="0.2"/>
<path d="M677.36,323.46s-14,5.89-18.8,11l-8.33,1.91H608.39L570,335.27a98.24,98.24,0,0,1,12.58-65.48c.3,0,8.89,2.15,17.47,32.21,0,0,8.53,31.81,22,30.4h23.5s21.08.73,21.08-13.23V297.7s.65-8.42,4.09-3.13L681.66,317S683.8,321.32,677.36,323.46Z" opacity="0.2"/>
<path d="M680.58,258c-8.42,6.71-12.77,17.28-12.88,27.91-.1-1.33-.27-2.68-.25-4,0-9.45,4.89-19.05,13.13-23.89Z" opacity="0.2"/>
<path d="M640.87,324h0a.54.54,0,0,1-.52-.55l1.07-32.21c0-21.29,5.35-36.51,5.4-36.66a.54.54,0,0,1,.69-.32.53.53,0,0,1,.32.68c0,.15-5.33,15.2-5.33,36.32l-1.08,32.22A.54.54,0,0,1,640.87,324Z" fill="#282728" data-secondary="true"/>
<path d="M614,327.22h0a.54.54,0,0,1-.52-.55l1.08-31.13a208.17,208.17,0,0,1,2.69-33.9.53.53,0,0,1,.62-.43.54.54,0,0,1,.43.63,208.45,208.45,0,0,0-2.67,33.71l-1.07,31.15A.55.55,0,0,1,614,327.22Z" fill="#282728" data-secondary="true"/>
<g opacity="0.2">
<path d="M640.87,324h0a.54.54,0,0,1-.52-.55l1.07-32.21c0-21.29,5.35-36.51,5.4-36.66a.54.54,0,0,1,.69-.32.53.53,0,0,1,.32.68c0,.15-5.33,15.2-5.33,36.32l-1.08,32.22A.54.54,0,0,1,640.87,324Z"/>
<path d="M614,327.22h0a.54.54,0,0,1-.52-.55l1.08-31.13a208.17,208.17,0,0,1,2.69-33.9.53.53,0,0,1,.62-.43.54.54,0,0,1,.43.63,208.45,208.45,0,0,0-2.67,33.71l-1.07,31.15A.55.55,0,0,1,614,327.22Z"/>
</g>
<path d="M706.34,371.77c0,.17,0,8.76-2.14,25.76l-.34,2.43a58.67,58.67,0,0,0-.52,7.27c-.06,3.84-2.56,11.15-21.3,8.21.49-.83.94-1.65,1.36-2.49l.35-.68c1.57-3,3.69-7.58,4.35-11.52,0,0-59-5.36-78.37-12.88,0,0-28.3-11.81-38.84-8.05l-8.39,7s-7.51-2.15,3.22-15Z" fill="#787878" data-primary="true"/>
<path d="M688.1,400.75s13.8.92,13.29,7a4,4,0,0,1-1.61,2.85c-1.84,1.41-6.25,3.29-16.15,1.89A63,63,0,0,0,688.1,400.75Z" opacity="0.2"/>
<path d="M564.14,385.44s-6.33-4,6.75-5.62Z" opacity="0.2"/>
<path d="M688.1,400.75a53.84,53.84,0,0,1-4.35,11.52l-.35.68c-.23.43-.44.81-.63,1.15s-.47.89-.73,1.34c-13.3,23.42-41.56,44.44-53.06,52.37a12.19,12.19,0,0,0-5.27,9.22c-2.38,36.84-19.34,68.64-19.34,68.64-3.61,5.17-9,7.66-15,8.46-19.1,2.58-45.12-11.68-45.12-11.68l14-39.72c11.81-44,23.61-56.89,23.61-56.89l26.54-55.12,1.37-2.85C629.06,395.39,688.1,400.75,688.1,400.75Z" fill="#282728" data-secondary="true"/>
<path d="M688.1,400.75a53.84,53.84,0,0,1-4.35,11.52l-.35.68c-.23.43-.44.81-.63,1.15s-.47.89-.73,1.34c-13.3,23.42-41.56,44.44-53.06,52.37a12.19,12.19,0,0,0-5.27,9.22c-2.38,36.84-19.34,68.64-19.34,68.64-3.61,5.17-9,7.66-15,8.46,20-25.72,31.27-83,31.27-83-.89-13.33,38.47-47.86,38.47-47.86,15-15-4.29-19.33-4.29-19.33-17.27-2.72-39.92-10.84-46.46-13.25l1.37-2.85C629.06,395.39,688.1,400.75,688.1,400.75Z" opacity="0.2"/>
<path d="M589.37,430.17l-33.31,13.52,13,26s-5.54,16.19-11.4,35.76c0,0-13.42,9.12-27.37,8.05,0,0-11.81-30.06-18.25-61.19,0,0-8.59-18.25,6.44-30.06L562,387.05l8.93-7.23s6.64-4.83,38.84,8.05Z" fill="#282728" data-secondary="true"/>
<path d="M547.62,532.88,526.35,549.3a27.56,27.56,0,0,1-6.81,3.93c-3.21,1.25-7.85,4-6.84,9.17a8.45,8.45,0,0,0,5.07,6.09c2.55,1.06,6.59,1.78,12.53.37l11.81-9.23,7.47-14.54-5.33-2.64Z" fill="#787878" data-primary="true"/>
<path d="M547.62,532.88,526.35,549.3a27.56,27.56,0,0,1-6.81,3.93c-3.21,1.25-7.85,4-6.84,9.17a8.45,8.45,0,0,0,5.07,6.09c2.55,1.06,6.59,1.78,12.53.37l11.81-9.23,7.47-14.54-5.33-2.64Z" opacity="0.2"/>
<path d="M589.37,430.17l-33.31,13.52,13,26a370.36,370.36,0,0,1-11.89,36s-12.93,8.88-26.88,7.81c39.72-3.22,20.78-67.22,20.78-67.22-4.38-9.78,2.19-12.65,6.18-13.48a53.87,53.87,0,0,0,7.5-2.12l10.06-3.7a29.52,29.52,0,0,0,16.91-15.53,27.2,27.2,0,0,0,2.23-8.31,14.25,14.25,0,0,0-10.3-15c-8.42-2.48-16.86-1.84-21.7-1.1l8.93-7.23s6.64-4.83,38.84,8.05Z" opacity="0.2"/>
<path d="M512.56,561c8,4.49,17.84,3.92,26,.25,1.19-.51,2.33-1.14,3.54-1.66-1.08.73-2.18,1.45-3.3,2.13-8,4.53-18.78,5.1-26.25-.72Z" opacity="0.2"/>
<path d="M524.65,550.52c4.14,1.84,12.18,6.84,12.1,11.9-.72-2.81-3.3-4.59-5.35-6.46s-4.5-3.57-6.75-5.44Z" opacity="0.2"/>
<path d="M530.3,546.25c3.1.44,5.52,3.24,6.45,6.1-2.23-2.05-4-4.27-6.45-6.1Z" opacity="0.2"/>
<path d="M534.32,543.14a9.55,9.55,0,0,1,6.62,6.16c-2.17-2.19-4.35-4-6.62-6.16Z" opacity="0.2"/>
<path d="M538.89,539.61a11.81,11.81,0,0,1,5.36,6.61,29.31,29.31,0,0,1-5.36-6.61Z" opacity="0.2"/>
<rect x="421.88" y="364.26" width="328.48" height="7.51" fill="#b3b3b3"/>
<polygon points="750.36 364.26 750.36 371.77 786.86 342.79 786.88 336.37 750.36 364.26" fill="#999"/>
<path d="M507.76,344.93h98.07l-7.33-63.74a5.61,5.61,0,0,0-5.57-5h-90a3,3,0,0,0-2.93,3.31Z" fill="#787878" data-primary="true"/>
<path d="M605.83,344.93H507.76L500,279.54a3,3,0,0,1,2.95-3.31h90a5.61,5.61,0,0,1,5.56,5Z" fill="#fff" opacity="0.3"/>
<polygon points="583.53 276.23 507.76 341.71 506.12 329.04 567.52 276.23 583.53 276.23" fill="#fff" opacity="0.3"/>
<path d="M517.07,344.93l79.55-67.31a6,6,0,0,1,1.88,3.57l.38,3.34-71.09,60.4Z" fill="#fff" opacity="0.3"/>
<rect x="507.76" y="344.93" width="94.46" height="6.44" fill="#787878" data-primary="true"/>
<rect x="602.22" y="344.93" width="29.49" height="6.44" fill="#787878" data-primary="true"/>
<rect x="602.22" y="344.93" width="29.49" height="6.44" opacity="0.2"/>
<polygon points="419.73 353.52 466.38 353.52 499.97 333.94 459.85 333.94 419.73 353.52" fill="#fff"/>
<rect x="419.73" y="353.52" width="46.65" height="4.65" fill="#e6e6e6"/>
<polygon points="499.97 333.94 499.97 339.8 466.38 358.17 466.38 353.52 499.97 333.94" fill="#ccc"/>
<polygon points="499.97 333.94 499.97 339.8 466.38 358.17 466.38 353.52 499.97 333.94" opacity="0.1"/>
<path d="M658.56,334.46s-13.47,1.87-20.95,12.08c0,0-10.05,9.15-.18,7.53,0,0,.47,4.68,8.39,1.53,0,0,1.37,3.31,10-1.53,0,0,8.64-4.84,16.16-7C672,347.08,675.17,334.71,658.56,334.46Z" fill="#f9b499"/>
<path d="M646,343.86a40.12,40.12,0,0,1-8.55,10.21A40.49,40.49,0,0,1,646,343.86Z" fill="#f7a48b"/>
<path d="M645.82,355.6a24.61,24.61,0,0,1,6.85-7.82,24.71,24.71,0,0,1-6.85,7.82Z" fill="#f7a48b"/>
<ellipse cx="638.72" cy="215.58" rx="6.44" ry="8.05" fill="#f9b499"/>
<path d="M640.87,228.89s12.2-4.93,24.24-3.72a26.56,26.56,0,0,1,17.33,9.17c4.85,5.6,11.54,15.1,4.38,18.3a8.59,8.59,0,0,1-7.29-.33c-5-2.49-17.91-6.91-47.79,12.65l27.37-23.08s6.49-6.48-3.5-8.53a13.52,13.52,0,0,0-2.62-.25,46.27,46.27,0,0,0-10,1.27S640.87,230.93,640.87,228.89Z" fill="#787878" data-primary="true"/>
<path d="M640.87,228.89s12.2-4.93,24.24-3.72a26.56,26.56,0,0,1,17.33,9.17c4.85,5.6,11.54,15.1,4.38,18.3a8.59,8.59,0,0,1-7.29-.33c-5-2.49-17.91-6.91-47.79,12.65l27.37-23.08s6.49-6.48-3.5-8.53a13.52,13.52,0,0,0-2.62-.25,46.27,46.27,0,0,0-10,1.27S640.87,230.93,640.87,228.89Z" fill="#fff" opacity="0.3"/>
<path d="M674.14,234.37c-5.73,6.95-13.48,12.06-21.25,16.49-1.15.58-2.28,1.2-3.44,1.76,8.36-5.92,17-11.41,24.69-18.25Z" fill="#fff" opacity="0.3"/>
<path d="M683.8,238.66C679,244,671.85,246.84,664.89,248c6.47-2.57,13.26-5.24,18.91-9.35Z" fill="#fff" opacity="0.3"/>
<path d="M625.84,265.5c-4.44-2.67-21.36-6.8-27.08-8.15a23.81,23.81,0,0,0-3.37-.5c-4.81-.45-3.9-3.16-3.9-3.16,0-4.29,6.63-7.09,6.63-7.09,6,6.11,13.48,9.26,20.74,8Z" fill="#787878" data-primary="true"/>
<path d="M625.84,265.5c-4.44-2.67-21.36-6.8-27.08-8.15a23.81,23.81,0,0,0-3.37-.5c-4.81-.45-3.9-3.16-3.9-3.16,0-4.29,6.63-7.09,6.63-7.09,6,6.11,13.48,9.26,20.74,8Z" fill="#fff" opacity="0.3"/>
<circle cx="551.23" cy="311.12" r="8.05" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 273 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -1,6 +1,4 @@
import type { SocialIcons } from "../types";
const socialIcons: SocialIcons = {
const socialIcons = {
Github: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
@ -57,14 +55,14 @@ const socialIcons: SocialIcons = {
<rect x="3" y="5" width="18" height="14" rx="2"></rect>
<polyline points="3 7 12 13 21 7"></polyline>
</svg>`,
Twitter: `<svg
X: `<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-tabler"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M22 4.01c-1 .49 -1.98 .689 -3 .99c-1.121 -1.265 -2.783 -1.335 -4.38 -.737s-2.643 2.06 -2.62 3.737v1c-3.245 .083 -6.135 -1.395 -8 -4c0 0 -4.182 7.433 4 11c-1.872 1.247 -3.739 2.088 -6 2c3.308 1.803 6.913 2.423 10.034 1.517c3.58 -1.04 6.522 -3.723 7.651 -7.742a13.84 13.84 0 0 0 .497 -3.753c-.002 -.249 1.51 -2.772 1.818 -4.013z"></path>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M4 4l11.733 16h4.267l-11.733 -16z" /><path d="M4 20l6.768 -6.768m2.46 -2.46l6.772 -6.772" />
</svg>`,
Twitch: `<svg
xmlns="http://www.w3.org/2000/svg"

View file

@ -10,13 +10,25 @@ const breadcrumbList = currentUrlPath.split("/").slice(1);
// replace Posts with Posts (page number)
breadcrumbList[0] === "posts" &&
breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`);
// if breadcrumb is Home > Tags > [tag] > [page] <etc>
// replace [tag] > [page] with [tag] (page number)
breadcrumbList[0] === "tags" &&
!isNaN(Number(breadcrumbList[2])) &&
breadcrumbList.splice(
1,
3,
`${breadcrumbList[1]} ${
Number(breadcrumbList[2]) === 1 ? "" : "(page " + breadcrumbList[2] + ")"
}`
);
---
<nav class="breadcrumb" aria-label="breadcrumb">
<ul>
<li>
<a href="/">Home</a>
<span aria-hidden="true">&nbsp;&gt;&nbsp;</span>
<span aria-hidden="true">&raquo;</span>
</li>
{
breadcrumbList.map((breadcrumb, index) =>
@ -27,13 +39,13 @@ breadcrumbList[0] === "posts" &&
aria-current="page"
>
{/* make the last part lowercase in Home > Tags > some-tag */}
{breadcrumb}
{decodeURIComponent(breadcrumb)}
</span>
</li>
) : (
<li>
<a href={`/${breadcrumb}`}>{breadcrumb}</a>
<span aria-hidden="true">&nbsp;&gt;&nbsp;</span>
<a href={`/${breadcrumb}/`}>{breadcrumb}</a>
<span aria-hidden="true">&raquo;</span>
</li>
)
)

View file

@ -9,7 +9,7 @@ export interface Props {
}
export default function Card({ href, frontmatter, secHeading = true }: Props) {
const { title, pubDatetime, description } = frontmatter;
const { title, pubDatetime, modDatetime, description } = frontmatter;
const headerProps = {
style: { viewTransitionName: slugifyStr(title) },
@ -28,7 +28,7 @@ export default function Card({ href, frontmatter, secHeading = true }: Props) {
<h3 {...headerProps}>{title}</h3>
)}
</a>
<Datetime datetime={pubDatetime} />
<Datetime pubDatetime={pubDatetime} modDatetime={modDatetime} />
<p>{description}</p>
</li>
);

View file

@ -1,52 +1,120 @@
import { LOCALE } from "@config";
import { LOCALE, SITE } from "@config";
import type { CollectionEntry } from "astro:content";
export interface Props {
datetime: string | Date;
interface DatetimesProps {
pubDatetime: string | Date;
modDatetime: string | Date | undefined | null;
}
interface EditPostProps {
editPost?: CollectionEntry<"blog">["data"]["editPost"];
postId?: CollectionEntry<"blog">["id"];
}
interface Props extends DatetimesProps, EditPostProps {
size?: "sm" | "lg";
className?: string;
}
export default function Datetime({ datetime, size = "sm", className }: Props) {
export default function Datetime({
pubDatetime,
modDatetime,
size = "sm",
className = "",
editPost,
postId,
}: Props) {
return (
<div className={`flex items-center space-x-2 opacity-80 ${className}`}>
<div
className={`flex items-center space-x-2 opacity-80 ${className}`.trim()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`${
size === "sm" ? "scale-90" : "scale-100"
} inline-block h-6 w-6 fill-skin-base`}
} inline-block h-6 w-6 min-w-[1.375rem] fill-skin-base`}
aria-hidden="true"
>
<path d="M7 11h2v2H7zm0 4h2v2H7zm4-4h2v2h-2zm0 4h2v2h-2zm4-4h2v2h-2zm0 4h2v2h-2z"></path>
<path d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM19 8l.001 12H5V8h14z"></path>
</svg>
<span className="sr-only">Posted on:</span>
{modDatetime && modDatetime > pubDatetime ? (
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
<FormattedDatetime datetime={datetime} />
Updated:
</span>
) : (
<span className="sr-only">Published:</span>
)}
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
<FormattedDatetime
pubDatetime={pubDatetime}
modDatetime={modDatetime}
/>
{size === "lg" && <EditPost editPost={editPost} postId={postId} />}
</span>
</div>
);
}
const FormattedDatetime = ({ datetime }: { datetime: string | Date }) => {
const myDatetime = new Date(datetime);
const FormattedDatetime = ({ pubDatetime, modDatetime }: DatetimesProps) => {
const myDatetime = new Date(
modDatetime && modDatetime > pubDatetime ? modDatetime : pubDatetime
);
const date = myDatetime.toLocaleDateString(LOCALE, {
const date = myDatetime.toLocaleDateString(LOCALE.langTag, {
year: "numeric",
month: "long",
month: "short",
day: "numeric",
});
const time = myDatetime.toLocaleTimeString(LOCALE, {
const time = myDatetime.toLocaleTimeString(LOCALE.langTag, {
hour: "2-digit",
minute: "2-digit",
});
return (
<>
{date}
<time dateTime={myDatetime.toISOString()}>{date}</time>
<span aria-hidden="true"> | </span>
<span className="sr-only">&nbsp;at&nbsp;</span>
{time}
<span className="text-nowrap">{time}</span>
</>
);
};
const EditPost = ({ editPost, postId }: EditPostProps) => {
let editPostUrl = editPost?.url ?? SITE?.editPost?.url ?? "";
const showEditPost = !editPost?.disabled && editPostUrl.length > 0;
const appendFilePath =
editPost?.appendFilePath ?? SITE?.editPost?.appendFilePath ?? false;
if (appendFilePath && postId) {
editPostUrl += `/${postId}`;
}
const editPostText = editPost?.text ?? SITE?.editPost?.text ?? "Edit";
return (
showEditPost && (
<>
<span aria-hidden="true"> | </span>
<a
className="space-x-1.5 hover:opacity-75"
href={editPostUrl}
rel="noopener noreferrer"
target="_blank"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon icon-tabler icons-tabler-outline icon-tabler-edit inline-block !scale-90 fill-skin-base"
aria-hidden="true"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" />
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" />
<path d="M16 5l3 3" />
</svg>
<span className="text-base italic">{editPostText}</span>
</a>
</>
)
);
};

View file

@ -16,7 +16,9 @@ const { noMarginTop = false } = Astro.props;
<div class="footer-wrapper">
<Socials centered />
<div class="copyright-wrapper">
<span>Copyright &#169 {currentYear}, EllieBotDevs</span>
<span>Copyright &#169; {currentYear} EllieBotDevs</span>
<span class="separator">&nbsp;|&nbsp;</span>
<span>All rights reserved.</span>
</div>
</div>
</footer>

View file

@ -4,7 +4,7 @@ import Hr from "./Hr.astro";
import LinkButton from "./LinkButton.astro";
export interface Props {
activeNav?: "posts" | "tags" | "about" | "search";
activeNav?: "posts" | "archives" | "tags" | "about" | "search";
}
const { activeNav } = Astro.props;
@ -56,23 +56,58 @@ const { activeNav } = Astro.props;
</button>
<ul id="menu-items" class="display-none sm:flex">
<li>
<a href="/posts" class={activeNav === "posts" ? "active" : ""}>
<a href="/posts/" class={activeNav === "posts" ? "active" : ""}>
Posts
</a>
</li>
<li>
<a href="/tags" class={activeNav === "tags" ? "active" : ""}>
<a href="/tags/" class={activeNav === "tags" ? "active" : ""}>
Tags
</a>
</li>
<li>
<a href="/about" class={activeNav === "about" ? "active" : ""}>
<a href="/about/" class={activeNav === "about" ? "active" : ""}>
About
</a>
</li>
{
SITE.showArchives && (
<li>
<LinkButton
href="/search"
href="/archives/"
className={`focus-outline flex justify-center p-3 sm:p-1`}
ariaLabel="archives"
title="Archives"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class:list={[
"icon icon-tabler icons-tabler-outline !hidden sm:!inline-block",
activeNav === "archives" && "!stroke-skin-accent",
]}
>
<>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" />
<path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10" />
<path d="M10 12l4 0" />
</>
</svg>
<span
class:list={[
"sm:sr-only",
activeNav === "archives" && "active",
]}
>
Archives
</span>
</LinkButton>
</li>
)
}
<li>
<LinkButton
href="/search/"
className={`focus-outline p-3 sm:p-1 ${
activeNav === "search" ? "active" : ""
} flex`}
@ -86,11 +121,12 @@ const { activeNav } = Astro.props;
d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"
></path>
</svg>
<span class="sr-only">Search</span>
</LinkButton>
</li>
<li>
{
SITE.lightAndDarkMode && (
<li>
<button
id="theme-btn"
class="focus-outline"
@ -105,9 +141,9 @@ const { activeNav } = Astro.props;
<path d="M6.993 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007S14.761 6.993 12 6.993 6.993 9.239 6.993 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19h2v3h-2zm0-17h2v3h-2zm-9 9h3v2h-3zm17 0h3v2h-3zM4.219 18.363l2.12-2.122 1.415 1.414-2.12 2.122zM16.24 6.344l2.122-2.122 1.414 1.414-2.122 2.122zM6.342 7.759 4.22 5.637l1.415-1.414 2.12 2.122zm13.434 10.605-1.414 1.414-2.122-2.122 1.414-1.414z" />
</svg>
</button>
</li>
)
}
</li>
</ul>
</nav>
</div>
@ -154,7 +190,7 @@ const { activeNav } = Astro.props;
nav ul li:nth-last-child(2) {
@apply col-span-1;
}
nav a.active {
nav .active {
@apply underline decoration-wavy decoration-2 underline-offset-4;
}
nav a.active svg {

View file

@ -7,22 +7,32 @@ export interface Props {
disabled?: boolean;
}
const { href, className, ariaLabel, title, disabled = false } = Astro.props;
const {
href,
className = "",
ariaLabel,
title,
disabled = false,
} = Astro.props;
---
<a
href={disabled ? "#" : href}
tabindex={disabled ? "-1" : "0"}
class={`group inline-block ${className}`}
aria-label={ariaLabel}
{
disabled ? (
<span
class:list={["group inline-block", className]}
title={title}
aria-disabled={disabled}
>
>
<slot />
</a>
<style>
a {
@apply hover:text-skin-accent;
}
</style>
</span>
) : (
<a
{href}
class:list={["group inline-block hover:text-skin-accent", className]}
aria-label={ariaLabel}
title={title}
>
<slot />
</a>
)
}

View file

@ -0,0 +1,59 @@
---
import type { Page } from "astro";
import LinkButton from "./LinkButton.astro";
import type { CollectionEntry } from "astro:content";
export interface Props {
page: Page<CollectionEntry<"blog">>;
}
const { page } = Astro.props;
---
{
page.lastPage > 1 && (
<nav class="pagination-wrapper" aria-label="Pagination">
<LinkButton
disabled={!page.url.prev}
href={page.url.prev as string}
className={`mr-4 select-none ${page.url.prev ? "" : "disabled"}`}
ariaLabel="Previous"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class:list={[{ "disabled-svg": !page.url.prev }]}
>
<path d="M12.707 17.293 8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z" />
</svg>
Prev
</LinkButton>
{page.currentPage} / {page.lastPage}
<LinkButton
disabled={!page.url.next}
href={page.url.next as string}
className={`mx-4 select-none ${page.url.next ? "" : "disabled"}`}
ariaLabel="Next"
>
Next
<svg
xmlns="http://www.w3.org/2000/svg"
class:list={[{ "disabled-svg": !page.url.next }]}
>
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z" />
</svg>
</LinkButton>
</nav>
)
}
<style>
.pagination-wrapper {
@apply mb-8 mt-auto flex justify-center;
}
.disabled {
@apply pointer-events-none select-none opacity-50 hover:text-skin-base group-hover:fill-skin-base;
}
.disabled-svg {
@apply group-hover:!fill-skin-base;
}
</style>

View file

@ -1,13 +1,13 @@
import Fuse from "fuse.js";
import { useEffect, useRef, useState, useMemo } from "react";
import { useEffect, useRef, useState, useMemo, type FormEvent } from "react";
import Card from "@components/Card";
import slugify from "@utils/slugify";
import type { CollectionEntry } from "astro:content";
export type SearchItem = {
title: string;
description: string;
data: CollectionEntry<"blog">["data"];
slug: string;
};
interface Props {
@ -26,7 +26,7 @@ export default function SearchBar({ searchList }: Props) {
null
);
const handleChange = (e: React.FormEvent<HTMLInputElement>) => {
const handleChange = (e: FormEvent<HTMLInputElement>) => {
setInputVal(e.currentTarget.value);
};
@ -58,7 +58,7 @@ export default function SearchBar({ searchList }: Props) {
useEffect(() => {
// Add search result only if
// input value is more than one character
let inputResult = inputVal.length > 1 ? fuse.search(inputVal) : [];
const inputResult = inputVal.length > 1 ? fuse.search(inputVal) : [];
setSearchResults(inputResult);
// Update search string in URL
@ -73,6 +73,13 @@ export default function SearchBar({ searchList }: Props) {
}
}, [inputVal]);
useEffect(() => {
// focus on text input when search bar is displayed
if (inputRef.current) {
inputRef.current.focus();
}
}, [inputVal]);
return (
<>
<label className="relative block">
@ -80,19 +87,17 @@ export default function SearchBar({ searchList }: Props) {
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"></path>
</svg>
<span className="sr-only">Search</span>
</span>
<input
className="block w-full rounded border border-skin-fill
border-opacity-40 bg-skin-fill py-3 pl-10
pr-3 placeholder:italic placeholder:text-opacity-75
focus:border-skin-accent focus:outline-none"
className="block w-full rounded border border-skin-fill/40 bg-skin-fill py-3 pl-10 pr-3 placeholder:italic focus:border-skin-accent focus:outline-none"
placeholder="Search for anything..."
type="text"
name="search"
value={inputVal}
onChange={handleChange}
autoComplete="off"
autoFocus
// autoFocus
ref={inputRef}
/>
</label>
@ -111,9 +116,9 @@ export default function SearchBar({ searchList }: Props) {
{searchResults &&
searchResults.map(({ item, refIndex }) => (
<Card
href={`/posts/${slugify(item.data)}`}
href={`/posts/${item.slug}/`}
frontmatter={item.data}
key={`${refIndex}-${slugify(item.data)}`}
key={`${refIndex}-${item.slug}`}
/>
))}
</ul>

View file

@ -0,0 +1,41 @@
---
import LinkButton from "./LinkButton.astro";
import socialIcons from "@assets/socialIcons";
const URL = Astro.url;
const shareLinks = [
{
name: "Mail",
href: "mailto:?subject=See%20this%20post&body=",
linkTitle: `Share this post via email`,
},
] as const;
---
<div class={`social-icons`}>
<span class="italic">Share this post on:</span>
<div class="text-center">
{
shareLinks.map(social => (
<LinkButton
href={`${social.href + URL}`}
className="link-button"
title={social.linkTitle}
>
<Fragment set:html={socialIcons[social.name]} />
<span class="sr-only">{social.linkTitle}</span>
</LinkButton>
))
}
</div>
</div>
<style>
.social-icons {
@apply flex flex-col flex-wrap items-center justify-center gap-1 sm:items-start;
}
.link-button {
@apply scale-90 p-2 hover:rotate-6 sm:p-1;
}
</style>

View file

@ -19,6 +19,7 @@ const { centered = false } = Astro.props;
title={social.linkTitle}
>
<Fragment set:html={socialIcons[social.name]} />
<span class="sr-only">{social.linkTitle}</span>
</LinkButton>
))
}

View file

@ -1,10 +1,10 @@
---
export interface Props {
name: string;
tag: string;
size?: "sm" | "lg";
}
const { name, size = "sm" } = Astro.props;
const { tag, size = "sm" } = Astro.props;
---
<li
@ -13,8 +13,8 @@ const { name, size = "sm" } = Astro.props;
}`}
>
<a
href={`/tags/${name.toLowerCase()}`}
transition:name={name.toLowerCase()}
href={`/tags/${tag}/`}
transition:name={tag}
class={`${size === "sm" ? "text-sm" : "text-lg"} pr-2 group`}
>
<svg
@ -24,7 +24,7 @@ const { name, size = "sm" } = Astro.props;
d="M16.018 3.815 15.232 8h-4.966l.716-3.815-1.964-.37L8.232 8H4v2h3.857l-.751 4H3v2h3.731l-.714 3.805 1.965.369L8.766 16h4.966l-.714 3.805 1.965.369.783-4.174H20v-2h-3.859l.751-4H21V8h-3.733l.716-3.815-1.965-.37zM14.106 14H9.141l.751-4h4.966l-.752 4z"
></path>
</svg>
&nbsp;<span>{name.toLowerCase()}</span>
&nbsp;<span>{tag}</span>
</a>
</li>

View file

@ -2,40 +2,52 @@ import type { Site, SocialObjects } from "./types";
export const SITE: Site = {
website: "https://blog.elliebot.net/", // replace this with your deployed domain
author: "toastie_t0ast, mai_lanfiel, EllieBot_Team",
author: "EllieBot Team",
profile: "https://blog.elliebot.net",
desc: "The tales of the Ellie dev team.",
title: "Elliebot blog",
ogImage: "",
lightAndDarkMode: true,
postPerPage: 3,
postPerIndex: 4,
postPerPage: 4,
scheduledPostMargin: 15 * 60 * 1000, // 15 minutes
showArchives: true,
editPost: {
url: "https://toastielab.dev/EllieBotDevs/Ellie-Blog/_edit/main/src/content/blog",
text: "Suggest Changes",
appendFilePath: true,
},
};
export const LOCALE = ["en-EN"]; // set to [] to use the environment default
export const LOCALE = {
lang: "en",
langTag: ["en-EN"],
};
export const LOGO_IMAGE = {
enable: false,
svg: true,
width: 216,
height: 46,
svg: false,
width: 210,
height: 40,
};
export const SOCIALS: SocialObjects = [
{
name: "Mail",
href: "mailto:contact@elliebot.net",
linkTitle: `Send an email to me`,
linkTitle: `Send an email to us`,
active: true,
},
{
name: "Discord",
href: "https://discord.gg/etQdZxSyEH",
linkTitle: `My Discord server`,
linkTitle: `Our Discord server`,
active: true,
},
{
name: "Mastodon",
href: "https://valkyriecoms.com/@EllieBotDevs",
linkTitle: `My profile on Valkyriecoms`,
linkTitle: `Our profile on Valkyriecoms`,
active: true,
},
];

View file

@ -2,6 +2,7 @@
author: toastie_t0ast
pubDatetime: 2022-11-14
title: Another update
slug: another-update
description: Another small update
tags:
- devlog

View file

@ -2,6 +2,7 @@
author: toastie_t0ast
pubDatetime: 2022-11-20
title: Ellie bday 2022
slug: ellie-bday-2022
description: Ellie bot and project birthday
tags:
- devlog

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

View file

@ -2,6 +2,7 @@
author: toastie_t0ast
pubDatetime: 2022-08-05
title: Small update
slug: small-update
description: small update on things
tags:
- devlog

View file

@ -2,6 +2,7 @@
author: mai_lanfiel
pubDatetime: 2022-04-02
title: State of development
slug: state-of-development
description: EllieBot state of development
tags:
- project-update

View file

@ -2,6 +2,7 @@
author: toastie_t0ast
pubDatetime: 2022-07-02
title: Update on things
slug: update-on-things
description: A small update on what we are up to.
tags:
- update

View file

@ -2,6 +2,7 @@
author: EllieBot_Team
pubDatetime: 2022-07-02
title: Updated links
slug: updated-links
description: About our updated links.
tags:
- announcement

View file

@ -2,6 +2,7 @@
author: toastie_t0ast, EllieBot_Team
pubDatetime: 2023-07-31
title: Where toastie has been
slug: where-toastie-has-been
description: Another small update
tags:
- devlog

View file

@ -0,0 +1,28 @@
---
author: toastie_t0ast
pubDatetime: 2025-01-22T01:13:22.000+13:00
modDatetime:
title: Where we have been
slug: where-we-have-been
featured: true
draft: false
tags:
- project=update
description: Where we have been and what we have been up to.
---
Hi there, it has been a while since I last posted to this blog, the last time I posted here was back in 2023 but I am here to let you all know where on earth we have been.
First of all where the heck we have been. We have been hard at work on some fun projects mostly giving Ellie even more fun features for more details you can check out our [patchnotes site](https://notes.elliebot.net) as well as making our docs site even better.
On the topic of the docs it has had a massive overhaul with new guides as well as an easy to follow guide on how to make your own marmalade module.
We have also overhauled the Discord server to make it easier to find information.
On where I personally have been, I have been studying full time and working on Ellie when I have spare time.
Thanks for reading this update on what we have been up to all this time.
Till next time,
Toastie

View file

@ -1,14 +1,16 @@
import { SITE } from "@config";
import { glob } from "astro/loaders";
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
type: "content_layer",
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
schema: ({ image }) =>
z.object({
author: z.string().default(SITE.author),
pubDatetime: z.date(),
modDatetime: z.date().optional().nullable(),
title: z.string(),
postSlug: z.string().optional(),
featured: z.boolean().optional(),
draft: z.boolean().optional(),
tags: z.array(z.string()).default(["others"]),
@ -20,6 +22,14 @@ const blog = defineCollection({
.optional(),
description: z.string(),
canonicalURL: z.string().optional(),
editPost: z
.object({
disabled: z.boolean().optional(),
url: z.string().optional(),
text: z.string().optional(),
appendFilePath: z.boolean().optional(),
})
.optional(),
}),
});

View file

@ -1,5 +1,5 @@
---
import { SITE } from "@config";
import { LOCALE, SITE } from "@config";
import "@styles/base.css";
import { ViewTransitions } from "astro:transitions";
@ -8,31 +8,58 @@ const googleSiteVerification = import.meta.env.PUBLIC_GOOGLE_SITE_VERIFICATION;
export interface Props {
title?: string;
author?: string;
profile?: string;
description?: string;
ogImage?: string;
canonicalURL?: string;
pubDatetime?: Date;
modDatetime?: Date | null;
scrollSmooth?: boolean;
}
const {
title = SITE.title,
author = SITE.author,
profile = SITE.profile,
description = SITE.desc,
ogImage = SITE.ogImage,
canonicalURL = new URL(Astro.url.pathname, Astro.site).href,
pubDatetime,
modDatetime,
scrollSmooth = false,
} = Astro.props;
const socialImageURL = new URL(
ogImage ?? SITE.ogImage ?? "og.png",
Astro.url.origin
).href;
const structuredData = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: `${title}`,
image: `${socialImageURL}`,
datePublished: `${pubDatetime?.toISOString()}`,
...(modDatetime && { dateModified: modDatetime.toISOString() }),
author: [
{
"@type": "Person",
name: `${author}`,
url: `${profile}`,
},
],
};
---
<!doctype html>
<html lang="en">
<html
lang=`${LOCALE.lang ?? "en"}`
class={`${scrollSmooth && "scroll-smooth"}`}
>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<link rel="canonical" href={canonicalURL} />
<meta name="generator" content={Astro.generator} />
@ -49,6 +76,24 @@ const socialImageURL = new URL(
<meta property="og:url" content={canonicalURL} />
<meta property="og:image" content={socialImageURL} />
<!-- Article Published/Modified time -->
{
pubDatetime && (
<meta
property="article:published_time"
content={pubDatetime.toISOString()}
/>
)
}
{
modDatetime && (
<meta
property="article:modified_time"
content={modDatetime.toISOString()}
/>
)
}
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonicalURL} />
@ -56,12 +101,21 @@ const socialImageURL = new URL(
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={socialImageURL} />
<!-- Google JSON-LD Structured data -->
<script
type="application/ld+json"
set:html={JSON.stringify(structuredData)}
/>
<!-- Google Font -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400;1,600&display=swap"
rel="stylesheet"
rel="preload"
as="style"
onload="this.onload=null; this.rel='stylesheet';"
crossorigin
/>
<meta name="theme-color" content="" />
@ -80,7 +134,7 @@ const socialImageURL = new URL(
<ViewTransitions />
<script is:inline src="/toggle-theme.js"></script>
<script is:inline src="/toggle-theme.js" async></script>
</head>
<body>
<slot />

View file

@ -37,7 +37,7 @@ const { props } = Astro;
<style>
#main-content {
@apply mx-auto w-full max-w-3xl px-4 pb-12;
@apply mx-auto w-full max-w-3xl px-4 pb-4;
}
#main-content h1 {
@apply text-2xl font-semibold sm:text-3xl;

View file

@ -6,15 +6,27 @@ import Tag from "@components/Tag.astro";
import Datetime from "@components/Datetime";
import type { CollectionEntry } from "astro:content";
import { slugifyStr } from "@utils/slugify";
import ShareLinks from "@components/ShareLinks.astro";
import { SITE } from "@config";
export interface Props {
post: CollectionEntry<"blog">;
posts: CollectionEntry<"blog">[];
}
const { post } = Astro.props;
const { post, posts } = Astro.props;
const { title, author, description, ogImage, canonicalURL, pubDatetime, tags } =
post.data;
const {
title,
author,
description,
ogImage,
canonicalURL,
pubDatetime,
modDatetime,
tags,
editPost,
} = post.data;
const { Content } = await post.render();
@ -23,20 +35,39 @@ const ogUrl = new URL(
ogImageUrl ?? `/posts/${slugifyStr(title)}.png`,
Astro.url.origin
).href;
const layoutProps = {
title: `${title} | ${SITE.title}`,
author,
description,
pubDatetime,
modDatetime,
canonicalURL,
ogImage: ogUrl,
scrollSmooth: true,
};
/* ========== Prev/Next Posts ========== */
const allPosts = posts.map(({ data: { title }, slug }) => ({
slug,
title,
}));
const currentPostIndex = allPosts.findIndex(a => a.slug === post.slug);
const prevPost = currentPostIndex !== 0 ? allPosts[currentPostIndex - 1] : null;
const nextPost =
currentPostIndex !== allPosts.length ? allPosts[currentPostIndex + 1] : null;
---
<Layout
title={title}
author={author}
description={description}
ogImage={ogUrl}
canonicalURL={canonicalURL}
>
<Layout {...layoutProps}>
<Header />
<div class="mx-auto flex w-full max-w-3xl justify-start px-2">
<button
class="focus-outline mb-2 mt-8 flex hover:opacity-75"
onclick="history.back()"
onclick="(() => (history.length === 1) ? window.location = '/' : history.back())()"
>
<svg xmlns="http://www.w3.org/2000/svg"
><path
@ -46,15 +77,108 @@ const ogUrl = new URL(
</button>
</div>
<main id="main-content">
<h1 transition:name={slugifyStr(title)} class="post-title">{title}</h1>
<Datetime datetime={pubDatetime} size="lg" className="my-2" />
<article id="article" role="article" class="prose mx-auto mt-8 max-w-3xl">
<h1 transition:name={slugifyStr(title)} class="post-title inline-block">
{title}
</h1>
<Datetime
pubDatetime={pubDatetime}
modDatetime={modDatetime}
size="lg"
className="my-2"
editPost={editPost}
postId={post.id}
/>
<article id="article" class="prose mx-auto mt-8 max-w-3xl">
<Content />
</article>
<ul class="tags-container">
{tags.map(tag => <Tag name={slugifyStr(tag)} />)}
<ul class="my-8">
{tags.map(tag => <Tag tag={slugifyStr(tag)} />)}
</ul>
<div
class="flex flex-col-reverse items-center justify-between gap-6 sm:flex-row-reverse sm:items-end sm:gap-4"
>
<button
id="back-to-top"
class="focus-outline whitespace-nowrap py-1 hover:opacity-75"
>
<svg xmlns="http://www.w3.org/2000/svg" class="rotate-90">
<path
d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"
></path>
</svg>
<span>Back to Top</span>
</button>
<ShareLinks />
</div>
<hr class="my-6 border-dashed" />
<!-- Previous/Next Post Buttons -->
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
{
prevPost && (
<a
href={`/posts/${prevPost.slug}`}
class="flex w-full gap-1 hover:opacity-75"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left flex-none"
>
<>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 6l-6 6l6 6" />
</>
</svg>
<div>
<span>Previous Post</span>
<div class="text-sm text-skin-accent/85">{prevPost.title}</div>
</div>
</a>
)
}
{
nextPost && (
<a
href={`/posts/${nextPost.slug}`}
class="flex w-full justify-end gap-1 text-right hover:opacity-75 sm:col-start-2"
>
<div>
<span>Next Post</span>
<div class="text-sm text-skin-accent/85">{nextPost.title}</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right flex-none"
>
<>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 6l6 6l-6 6" />
</>
</svg>
</a>
)
}
</div>
</main>
<Footer />
</Layout>
@ -66,7 +190,126 @@ const ogUrl = new URL(
.post-title {
@apply text-2xl font-semibold text-skin-accent;
}
.tags-container {
@apply my-8;
}
</style>
<script is:inline data-astro-rerun>
/** Create a progress indicator
* at the top */
function createProgressBar() {
// Create the main container div
const progressContainer = document.createElement("div");
progressContainer.className =
"progress-container fixed top-0 z-10 h-1 w-full bg-skin-fill";
// Create the progress bar div
const progressBar = document.createElement("div");
progressBar.className = "progress-bar h-1 w-0 bg-skin-accent";
progressBar.id = "myBar";
// Append the progress bar to the progress container
progressContainer.appendChild(progressBar);
// Append the progress container to the document body or any other desired parent element
document.body.appendChild(progressContainer);
}
createProgressBar();
/** Update the progress bar
* when user scrolls */
function updateScrollProgress() {
document.addEventListener("scroll", () => {
const winScroll =
document.body.scrollTop || document.documentElement.scrollTop;
const height =
document.documentElement.scrollHeight -
document.documentElement.clientHeight;
const scrolled = (winScroll / height) * 100;
if (document) {
const myBar = document.getElementById("myBar");
if (myBar) {
myBar.style.width = scrolled + "%";
}
}
});
}
updateScrollProgress();
/** Attaches links to headings in the document,
* allowing sharing of sections easily */
function addHeadingLinks() {
const headings = Array.from(
document.querySelectorAll("h2, h3, h4, h5, h6")
);
for (const heading of headings) {
heading.classList.add("group");
const link = document.createElement("a");
link.className =
"heading-link ml-2 opacity-0 group-hover:opacity-100 focus:opacity-100";
link.href = "#" + heading.id;
const span = document.createElement("span");
span.ariaHidden = "true";
span.innerText = "#";
link.appendChild(span);
heading.appendChild(link);
}
}
addHeadingLinks();
/** Attaches copy buttons to code blocks in the document,
* allowing users to copy code easily. */
function attachCopyButtons() {
const copyButtonLabel = "Copy";
const codeBlocks = Array.from(document.querySelectorAll("pre"));
for (const codeBlock of codeBlocks) {
const wrapper = document.createElement("div");
wrapper.style.position = "relative";
const copyButton = document.createElement("button");
copyButton.className =
"copy-code absolute right-3 -top-3 rounded bg-skin-card px-2 py-1 text-xs leading-4 text-skin-base font-medium";
copyButton.innerHTML = copyButtonLabel;
codeBlock.setAttribute("tabindex", "0");
codeBlock.appendChild(copyButton);
// wrap codebock with relative parent element
codeBlock?.parentNode?.insertBefore(wrapper, codeBlock);
wrapper.appendChild(codeBlock);
copyButton.addEventListener("click", async () => {
await copyCode(codeBlock, copyButton);
});
}
async function copyCode(block, button) {
const code = block.querySelector("code");
const text = code?.innerText;
await navigator.clipboard.writeText(text ?? "");
// visual feedback that task is completed
button.innerText = "Copied";
setTimeout(() => {
button.innerText = copyButtonLabel;
}, 700);
}
}
attachCopyButtons();
/** Scrolls the document to the top when
* the "Back to Top" button is clicked. */
function backToTop() {
document.querySelector("#back-to-top")?.addEventListener("click", () => {
document.body.scrollTop = 0; // For Safari
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
});
}
backToTop();
/* Go to page start after page swap */
document.addEventListener("astro:after-swap", () =>
window.scrollTo({ left: 0, top: 0, behavior: "instant" })
);
</script>

View file

@ -1,24 +1,19 @@
---
import { SITE } from "@config";
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Pagination from "@components/Pagination.astro";
import Card from "@components/Card";
import LinkButton from "@components/LinkButton.astro";
import slugify from "@utils/slugify";
import { SITE } from "@config";
import type { Page } from "astro";
import type { CollectionEntry } from "astro:content";
export interface Props {
pageNum: number;
totalPages: number;
posts: CollectionEntry<"blog">[];
page: Page<CollectionEntry<"blog">>;
}
const { pageNum, totalPages, posts } = Astro.props;
const prev = pageNum > 1 ? "" : "disabled";
const next = pageNum < totalPages ? "" : "disabled";
const { page } = Astro.props;
---
<Layout title={`Posts | ${SITE.title}`}>
@ -26,52 +21,14 @@ const next = pageNum < totalPages ? "" : "disabled";
<Main pageTitle="Posts" pageDesc="All the articles I've posted.">
<ul>
{
posts.map(({ data }) => (
<Card href={`/posts/${slugify(data)}`} frontmatter={data} />
page.data.map(({ data, slug }) => (
<Card href={`/posts/${slug}/`} frontmatter={data} />
))
}
</ul>
</Main>
{
totalPages > 1 && (
<nav class="pagination-wrapper" aria-label="Pagination">
<LinkButton
disabled={prev === "disabled"}
href={`/posts${pageNum - 1 !== 1 ? "/" + (pageNum - 1) : ""}`}
className={`mr-4 select-none ${prev}`}
ariaLabel="Previous"
>
<svg xmlns="http://www.w3.org/2000/svg" class={`${prev}-svg`}>
<path d="M12.707 17.293 8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z" />
</svg>
Prev
</LinkButton>
<LinkButton
disabled={next === "disabled"}
href={`/posts/${pageNum + 1}`}
className={`ml-4 select-none ${next}`}
ariaLabel="Next"
>
Next
<svg xmlns="http://www.w3.org/2000/svg" class={`${next}-svg`}>
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z" />
</svg>
</LinkButton>
</nav>
)
}
<Footer noMarginTop={totalPages > 1} />
</Layout>
<Pagination {page} />
<style>
.pagination-wrapper {
@apply mb-8 mt-auto flex justify-center;
}
.disabled {
@apply pointer-events-none select-none opacity-50 hover:text-skin-base group-hover:fill-skin-base;
}
.disabled-svg {
@apply group-hover:!fill-skin-base;
}
</style>
<Footer noMarginTop={page.lastPage > 1} />
</Layout>

View file

@ -0,0 +1,41 @@
---
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Card from "@components/Card";
import Pagination from "@components/Pagination.astro";
import { SITE } from "@config";
import type { Page } from "astro";
import type { CollectionEntry } from "astro:content";
export interface Props {
page: Page<CollectionEntry<"blog">>;
tag: string;
tagName: string;
}
const { page, tag, tagName } = Astro.props;
---
<Layout title={`Tag: ${tagName} | ${SITE.title}`}>
<Header activeNav="tags" />
<Main
pageTitle={[`Tag:`, `${tagName}`]}
titleTransition={tag}
pageDesc={`All the articles with the tag "${tagName}".`}
>
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
<ul>
{
page.data.map(({ data, slug }) => (
<Card href={`/posts/${slug}/`} frontmatter={data} />
))
}
</ul>
</Main>
<Pagination {page} />
<Footer noMarginTop={page.lastPage > 1} />
</Layout>

View file

@ -11,12 +11,12 @@ import LinkButton from "@components/LinkButton.astro";
<main id="main-content">
<div class="not-found-wrapper">
<h1 aria-label="404 Not Found">404</h1>
<h1>404</h1>
<span aria-hidden="true">It seems like you are lost</span>
<p>Let's get you back home</p>
<LinkButton
href="/"
className="my-6 underline decoration-dashed underline-offset-8 text-lg"
className="my-6 text-lg underline decoration-dashed underline-offset-8"
>
Go back home
</LinkButton>

View file

@ -0,0 +1,84 @@
---
import { getCollection } from "astro:content";
import Card from "@components/Card";
import Footer from "@components/Footer.astro";
import Header from "@components/Header.astro";
import { SITE } from "@config";
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import getPostsByGroupCondition from "@utils/getPostsByGroupCondition";
// Redirect to 404 page if `showArchives` config is false
if (!SITE.showArchives) {
return Astro.redirect("/404");
}
const posts = await getCollection("blog", ({ data }) => !data.draft);
const MonthMap: Record<string, string> = {
"1": "January",
"2": "February",
"3": "March",
"4": "April",
"5": "May",
"6": "June",
"7": "July",
"8": "August",
"9": "September",
"10": "October",
"11": "November",
"12": "December",
};
---
<Layout title={`Archives | ${SITE.title}`}>
<Header activeNav="archives" />
<Main pageTitle="Archives" pageDesc="All the articles I've archived.">
{
Object.entries(
getPostsByGroupCondition(posts, post =>
post.data.pubDatetime.getFullYear()
)
)
.sort(([yearA], [yearB]) => Number(yearB) - Number(yearA))
.map(([year, yearGroup]) => (
<div>
<span class="text-2xl font-bold">{year}</span>
<sup class="text-sm">{yearGroup.length}</sup>
{Object.entries(
getPostsByGroupCondition(
yearGroup,
post => post.data.pubDatetime.getMonth() + 1
)
)
.sort(([monthA], [monthB]) => Number(monthB) - Number(monthA))
.map(([month, monthGroup]) => (
<div class="flex flex-col sm:flex-row">
<div class="mt-6 min-w-36 text-lg sm:my-6">
<span class="font-bold">{MonthMap[month]}</span>
<sup class="text-xs">{monthGroup.length}</sup>
</div>
<ul>
{monthGroup
.sort(
(a, b) =>
Math.floor(
new Date(b.data.pubDatetime).getTime() / 1000
) -
Math.floor(
new Date(a.data.pubDatetime).getTime() / 1000
)
)
.map(({ data, slug }) => (
<Card href={`/posts/${slug}`} frontmatter={data} />
))}
</ul>
</div>
))}
</div>
))
}
</Main>
<Footer />
</Layout>

View file

@ -8,13 +8,13 @@ import Hr from "@components/Hr.astro";
import Card from "@components/Card";
import Socials from "@components/Socials.astro";
import getSortedPosts from "@utils/getSortedPosts";
import slugify from "@utils/slugify";
import { SOCIALS } from "@config";
import { SITE, SOCIALS } from "@config";
const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts);
const featuredPosts = sortedPosts.filter(({ data }) => data.featured);
const recentPosts = sortedPosts.filter(({ data }) => !data.featured);
const socialCount = SOCIALS.filter(social => social.active).length;
---
@ -23,8 +23,7 @@ const socialCount = SOCIALS.filter(social => social.active).length;
<Header />
<main id="main-content">
<section id="hero">
<h1 class="mr-2">Welcome!
</h1>
<h1>Welcome!</h1>
<a
target="_blank"
href="/rss.xml"
@ -39,12 +38,13 @@ const socialCount = SOCIALS.filter(social => social.active).length;
d="M12 20.001h2C14 14.486 9.514 10 4 10v2c4.411 0 8 3.589 8 8.001z"
></path><circle cx="6" cy="18" r="2"></circle>
</svg>
<span class="sr-only">RSS Feed</span>
</a>
<p>
Hi there, it looks like you have stumbled upon our blog,
please go and get something to drink or something to eat
while you read the contents of this blog.
Hi there, it looks like you have stumbled upon our blog, please go and
get something to drink or something to eat while you read the contents
of this blog.
</p>
{
// only display if at least one social link is enabled
@ -65,40 +65,42 @@ const socialCount = SOCIALS.filter(social => social.active).length;
<section id="featured">
<h2>Featured</h2>
<ul>
{featuredPosts.map(({ data }) => (
{featuredPosts.map(({ data, slug }) => (
<Card
href={`/posts/${slugify(data)}`}
href={`/posts/${slug}/`}
frontmatter={data}
secHeading={false}
/>
))}
</ul>
</section>
<Hr />
{recentPosts.length > 0 && <Hr />}
</>
)
}
{
recentPosts.length > 0 && (
<section id="recent-posts">
<h2>Recent Posts</h2>
<ul>
{
sortedPosts
.filter(({ data }) => !data.featured)
.map(
({ data }, index) =>
index < 4 && (
{recentPosts.map(
({ data, slug }, index) =>
index < SITE.postPerIndex && (
<Card
href={`/posts/${slugify(data)}`}
href={`/posts/${slug}/`}
frontmatter={data}
secHeading={false}
/>
)
)}
</ul>
</section>
)
}
</ul>
<div class="all-posts-btn-wrapper">
<LinkButton href="/posts">
<LinkButton href="/posts/">
All Posts
<svg xmlns="http://www.w3.org/2000/svg"
><path
@ -107,7 +109,6 @@ const socialCount = SOCIALS.filter(social => social.active).length;
</svg>
</LinkButton>
</div>
</section>
</main>
<Footer />

View file

@ -0,0 +1,16 @@
---
import { SITE } from "@config";
import Posts from "@layouts/Posts.astro";
import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content";
import getSortedPosts from "@utils/getSortedPosts";
export const getStaticPaths = (async ({ paginate }) => {
const posts = await getCollection("blog", ({ data }) => !data.draft);
return paginate(getSortedPosts(posts), { pageSize: SITE.postPerPage });
}) satisfies GetStaticPaths;
const { page } = Astro.props;
---
<Posts {page} />

View file

@ -1,11 +1,7 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import Posts from "@layouts/Posts.astro";
import PostDetails from "@layouts/PostDetails.astro";
import getSortedPosts from "@utils/getSortedPosts";
import getPageNumbers from "@utils/getPageNumbers";
import slugify from "@utils/slugify";
import { SITE } from "@config";
export interface Props {
post: CollectionEntry<"blog">;
@ -15,44 +11,17 @@ export async function getStaticPaths() {
const posts = await getCollection("blog", ({ data }) => !data.draft);
const postResult = posts.map(post => ({
params: { slug: slugify(post.data) },
params: { slug: post.slug },
props: { post },
}));
const pagePaths = getPageNumbers(posts.length).map(pageNum => ({
params: { slug: String(pageNum) },
}));
return [...postResult, ...pagePaths];
return postResult;
}
const { slug } = Astro.params;
const { post } = Astro.props;
const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts);
const totalPages = getPageNumbers(sortedPosts.length);
const currentPage =
slug && !isNaN(Number(slug)) && totalPages.includes(Number(slug))
? Number(slug)
: 0;
const lastPost = currentPage * SITE.postPerPage;
const startPost = lastPost - SITE.postPerPage;
const paginatedPosts = sortedPosts.slice(startPost, lastPost);
---
{
post ? (
<PostDetails post={post} />
) : (
<Posts
posts={paginatedPosts}
pageNum={currentPage}
totalPages={totalPages.length}
/>
)
}
<PostDetails post={post} posts={sortedPosts} />

View file

@ -1,18 +0,0 @@
---
import { SITE } from "@config";
import Posts from "@layouts/Posts.astro";
import getSortedPosts from "@utils/getSortedPosts";
import getPageNumbers from "@utils/getPageNumbers";
import { getCollection } from "astro:content";
const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts);
const totalPages = getPageNumbers(sortedPosts.length);
const paginatedPosts = sortedPosts.slice(0, SITE.postPerPage);
---
<Posts posts={paginatedPosts} pageNum={1} totalPages={totalPages.length} />

View file

@ -1,7 +1,6 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import getSortedPosts from "@utils/getSortedPosts";
import slugify from "@utils/slugify";
import { SITE } from "@config";
export async function GET() {
@ -11,11 +10,11 @@ export async function GET() {
title: SITE.title,
description: SITE.desc,
site: SITE.website,
items: sortedPosts.map(({ data }) => ({
link: `posts/${slugify(data)}`,
items: sortedPosts.map(({ data, slug }) => ({
link: `posts/${slug}/`,
title: data.title,
description: data.description,
pubDate: new Date(data.pubDatetime),
pubDate: new Date(data.modDatetime ?? data.pubDatetime),
})),
});
}

View file

@ -6,15 +6,18 @@ import Main from "@layouts/Main.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import SearchBar from "@components/Search";
import getSortedPosts from "@utils/getSortedPosts";
// Retrieve all articles
// Retrieve all published articles
const posts = await getCollection("blog", ({ data }) => !data.draft);
const sortedPosts = getSortedPosts(posts);
// List of items to search in
const searchList = posts.map(({ data }) => ({
const searchList = sortedPosts.map(({ data, slug }) => ({
title: data.title,
description: data.description,
data,
slug,
}));
---

View file

@ -1,58 +0,0 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Card from "@components/Card";
import getUniqueTags from "@utils/getUniqueTags";
import getPostsByTag from "@utils/getPostsByTag";
import slugify from "@utils/slugify";
import { SITE } from "@config";
import getSortedPosts from "@utils/getSortedPosts";
export interface Props {
post: CollectionEntry<"blog">;
tag: string;
}
export async function getStaticPaths() {
const posts = await getCollection("blog");
const tags = getUniqueTags(posts);
return tags.map(tag => {
return {
params: { tag },
props: { tag },
};
});
}
const { tag } = Astro.props;
const posts = await getCollection("blog", ({ data }) => !data.draft);
const tagPosts = getPostsByTag(posts, tag);
const sortTagsPost = getSortedPosts(tagPosts);
---
<Layout title={`Tag:${tag} | ${SITE.title}`}>
<Header activeNav="tags" />
<Main
pageTitle={[`Tag:`, `${tag}`]}
titleTransition={tag}
pageDesc={`All the articles with the tag "${tag}".`}
>
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
<ul>
{
sortTagsPost.map(({ data }) => (
<Card href={`/posts/${slugify(data)}`} frontmatter={data} />
))
}
</ul>
</Main>
<Footer />
</Layout>

View file

@ -0,0 +1,29 @@
---
import { getCollection } from "astro:content";
import TagPosts from "@layouts/TagPosts.astro";
import getUniqueTags from "@utils/getUniqueTags";
import getPostsByTag from "@utils/getPostsByTag";
import type { GetStaticPathsOptions } from "astro";
import { SITE } from "@config";
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const posts = await getCollection("blog");
const tags = getUniqueTags(posts);
return tags.flatMap(({ tag, tagName }) => {
const tagPosts = getPostsByTag(posts, tag);
return paginate(tagPosts, {
params: { tag },
props: { tagName },
pageSize: SITE.postPerPage,
});
});
}
const params = Astro.params;
const { tag } = params;
const { page, tagName } = Astro.props;
---
<TagPosts {page} {tag} {tagName} />

View file

@ -17,7 +17,7 @@ let tags = getUniqueTags(posts);
<Header activeNav="tags" />
<Main pageTitle="Tags" pageDesc="All the tags used in posts.">
<ul>
{tags.map(tag => <Tag name={tag} size="lg" />)}
{tags.map(({ tag }) => <Tag {tag} size="lg" />)}
</ul>
</Main>
<Footer />

View file

@ -5,20 +5,20 @@
@layer base {
:root,
html[data-theme="light"] {
--color-fill: 251, 254, 251;
--color-text-base: 40, 39, 40;
--color-accent: 0, 108, 172;
--color-card: 230, 230, 230;
--color-card-muted: 205, 205, 205;
--color-border: 236, 233, 233;
--color-fill: 242, 245, 236;
--color-text-base: 53, 53, 56;
--color-accent: 17, 88, 209;
--color-card: 206, 213, 180;
--color-card-muted: 187, 199, 137;
--color-border: 124, 173, 255;
}
html[data-theme="dark"] {
--color-fill: 33, 39, 55;
--color-fill: 0, 1, 35;
--color-accent: 97, 123, 255;
--color-text-base: 234, 237, 243;
--color-accent: 255, 107, 1;
--color-card: 52, 63, 96;
--color-card-muted: 138, 51, 2;
--color-border: 171, 75, 8;
--color-card: 33, 34, 83;
--color-card-muted: 12, 14, 79;
--color-border: 48, 63, 138;
}
#sun-svg,
html[data-theme="dark"] #moon-svg {
@ -29,45 +29,23 @@
display: block;
}
body {
@apply flex min-h-[100svh] flex-col bg-skin-fill font-mono text-skin-base
selection:bg-skin-accent selection:bg-opacity-70 selection:text-skin-inverted;
@apply flex min-h-[100svh] flex-col bg-skin-fill font-mono text-skin-base selection:bg-skin-accent/70 selection:text-skin-inverted;
}
section,
footer {
@apply mx-auto max-w-3xl px-4;
}
a {
@apply outline-2 outline-offset-1 outline-skin-fill
focus-visible:no-underline focus-visible:outline-dashed;
@apply outline-2 outline-offset-1 outline-skin-fill focus-visible:no-underline focus-visible:outline-dashed;
}
svg {
@apply inline-block h-6 w-6 fill-skin-base group-hover:fill-skin-accent;
}
svg.icon-tabler {
@apply inline-block h-6 w-6 scale-125 fill-transparent
stroke-current stroke-2 opacity-90 group-hover:fill-transparent
sm:scale-110;
@apply inline-block h-6 w-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110;
}
.prose {
@apply prose-headings:!mb-3 prose-headings:!text-skin-base
prose-h3:italic prose-p:!text-skin-base
prose-a:!text-skin-base prose-a:!decoration-dashed prose-a:underline-offset-8
hover:prose-a:text-skin-accent prose-blockquote:!border-l-skin-accent
prose-blockquote:border-opacity-50 prose-blockquote:opacity-80
prose-figcaption:!text-skin-base prose-figcaption:opacity-70
prose-strong:!text-skin-base
prose-code:rounded prose-code:bg-skin-card
prose-code:bg-opacity-75 prose-code:p-1 prose-code:!text-skin-base
prose-code:before:!content-[''] prose-code:after:!content-['']
prose-pre:!text-skin-base prose-ol:!text-skin-base
prose-ul:overflow-x-clip prose-ul:!text-skin-base prose-li:marker:!text-skin-accent
prose-table:text-skin-base prose-th:border
prose-th:border-skin-line prose-td:border
prose-td:border-skin-line prose-img:mx-auto
prose-img:!mt-2 prose-img:border-2
prose-img:border-skin-line prose-hr:!border-skin-line;
@apply prose-headings:!mb-3 prose-headings:!text-skin-base prose-h3:italic prose-p:!text-skin-base prose-a:!text-skin-base prose-a:!decoration-dashed prose-a:underline-offset-8 hover:prose-a:text-skin-accent prose-blockquote:!border-l-skin-accent/50 prose-blockquote:opacity-80 prose-figcaption:!text-skin-base prose-figcaption:opacity-70 prose-strong:!text-skin-base prose-code:rounded prose-code:bg-skin-card/75 prose-code:p-1 prose-code:before:!content-none prose-code:after:!content-none prose-ol:!text-skin-base prose-ul:overflow-x-clip prose-ul:!text-skin-base prose-li:marker:!text-skin-accent prose-table:text-skin-base prose-th:border prose-th:border-skin-line prose-td:border prose-td:border-skin-line prose-img:!my-2 prose-img:mx-auto prose-img:border-2 prose-img:border-skin-line prose-hr:!border-skin-line;
}
.prose a {
@apply break-words hover:!text-skin-accent;
@ -115,6 +93,10 @@
@apply bg-skin-card-muted;
}
/* ===== Code Blocks & Syntax Highlighting ===== */
pre:has(code) {
@apply border border-skin-line;
}
code,
blockquote {
word-wrap: break-word;
@ -123,6 +105,16 @@
pre > code {
white-space: pre;
}
/* Apply Dark Theme (if multi-theme specified) */
html[data-theme="dark"] pre:has(code),
html[data-theme="dark"] pre:has(code) span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
}
@layer components {

View file

@ -1,42 +1,27 @@
import type socialIcons from "@assets/socialIcons";
export type Site = {
website: string;
author: string;
profile: string;
desc: string;
title: string;
ogImage?: string;
lightAndDarkMode: boolean;
postPerIndex: number;
postPerPage: number;
scheduledPostMargin: number;
showArchives?: boolean;
editPost?: {
url?: URL["href"];
text?: string;
appendFilePath?: boolean;
};
};
export type SocialObjects = {
name: SocialMedia;
name: keyof typeof socialIcons;
href: string;
active: boolean;
linkTitle: string;
}[];
export type SocialIcons = {
[social in SocialMedia]: string;
};
export type SocialMedia =
| "Github"
| "Facebook"
| "Instagram"
| "LinkedIn"
| "Mail"
| "Twitter"
| "Twitch"
| "YouTube"
| "WhatsApp"
| "Snapchat"
| "Pinterest"
| "TikTok"
| "CodePen"
| "Discord"
| "GitLab"
| "Reddit"
| "Skype"
| "Steam"
| "Telegram"
| "Mastodon";

View file

@ -1,47 +1,8 @@
import satori, { type SatoriOptions } from "satori";
import { Resvg } from "@resvg/resvg-js";
import { type CollectionEntry } from "astro:content";
import postOgImage from "./og-templates/post";
import siteOgImage from "./og-templates/site";
const fetchFonts = async () => {
// Regular Font
const fontFileRegular = await fetch(
"https://www.1001fonts.com/download/font/ibm-plex-mono.regular.ttf"
);
const fontRegular: ArrayBuffer = await fontFileRegular.arrayBuffer();
// Bold Font
const fontFileBold = await fetch(
"https://www.1001fonts.com/download/font/ibm-plex-mono.bold.ttf"
);
const fontBold: ArrayBuffer = await fontFileBold.arrayBuffer();
return { fontRegular, fontBold };
};
const { fontRegular, fontBold } = await fetchFonts();
const options: SatoriOptions = {
width: 1200,
height: 630,
embedFont: true,
fonts: [
{
name: "IBM Plex Mono",
data: fontRegular,
weight: 400,
style: "normal",
},
{
name: "IBM Plex Mono",
data: fontBold,
weight: 600,
style: "normal",
},
],
};
function svgBufferToPngBuffer(svg: string) {
const resvg = new Resvg(svg);
const pngData = resvg.render();
@ -49,11 +10,11 @@ function svgBufferToPngBuffer(svg: string) {
}
export async function generateOgImageForPost(post: CollectionEntry<"blog">) {
const svg = await satori(postOgImage(post), options);
const svg = await postOgImage(post);
return svgBufferToPngBuffer(svg);
}
export async function generateOgImageForSite() {
const svg = await satori(siteOgImage(), options);
const svg = await siteOgImage();
return svgBufferToPngBuffer(svg);
}

View file

@ -1,14 +0,0 @@
import { SITE } from "@config";
const getPageNumbers = (numberOfPosts: number) => {
const numberOfPages = numberOfPosts / Number(SITE.postPerPage);
let pageNumbers: number[] = [];
for (let i = 1; i <= Math.ceil(numberOfPages); i++) {
pageNumbers = [...pageNumbers, i];
}
return pageNumbers;
};
export default getPageNumbers;

View file

@ -0,0 +1,25 @@
import type { CollectionEntry } from "astro:content";
type GroupKey = string | number | symbol;
interface GroupFunction<T> {
(item: T, index?: number): GroupKey;
}
const getPostsByGroupCondition = (
posts: CollectionEntry<"blog">[],
groupFunction: GroupFunction<CollectionEntry<"blog">>
) => {
const result: Record<GroupKey, CollectionEntry<"blog">[]> = {};
for (let i = 0; i < posts.length; i++) {
const item = posts[i];
const groupKey = groupFunction(item, i);
if (!result[groupKey]) {
result[groupKey] = [];
}
result[groupKey].push(item);
}
return result;
};
export default getPostsByGroupCondition;

View file

@ -1,7 +1,10 @@
import { slugifyAll } from "./slugify";
import type { CollectionEntry } from "astro:content";
import getSortedPosts from "./getSortedPosts";
import { slugifyAll } from "./slugify";
const getPostsByTag = (posts: CollectionEntry<"blog">[], tag: string) =>
posts.filter(post => slugifyAll(post.data.tags).includes(tag));
getSortedPosts(
posts.filter(post => slugifyAll(post.data.tags).includes(tag))
);
export default getPostsByTag;

View file

@ -1,12 +1,18 @@
import type { CollectionEntry } from "astro:content";
import postFilter from "./postFilter";
const getSortedPosts = (posts: CollectionEntry<"blog">[]) =>
posts
.filter(({ data }) => !data.draft)
const getSortedPosts = (posts: CollectionEntry<"blog">[]) => {
return posts
.filter(postFilter)
.sort(
(a, b) =>
Math.floor(new Date(b.data.pubDatetime).getTime() / 1000) -
Math.floor(new Date(a.data.pubDatetime).getTime() / 1000)
Math.floor(
new Date(b.data.modDatetime ?? b.data.pubDatetime).getTime() / 1000
) -
Math.floor(
new Date(a.data.modDatetime ?? a.data.pubDatetime).getTime() / 1000
)
);
};
export default getSortedPosts;

View file

@ -1,16 +1,22 @@
import { slugifyStr } from "./slugify";
import type { CollectionEntry } from "astro:content";
import postFilter from "./postFilter";
interface Tag {
tag: string;
tagName: string;
}
const getUniqueTags = (posts: CollectionEntry<"blog">[]) => {
const filteredPosts = posts.filter(({ data }) => !data.draft);
const tags: string[] = filteredPosts
const tags: Tag[] = posts
.filter(postFilter)
.flatMap(post => post.data.tags)
.map(tag => slugifyStr(tag))
.map(tag => ({ tag: slugifyStr(tag), tagName: tag }))
.filter(
(value: string, index: number, self: string[]) =>
self.indexOf(value) === index
(value, index, self) =>
self.findIndex(tag => tag.tag === value.tag) === index
)
.sort((tagA: string, tagB: string) => tagA.localeCompare(tagB));
.sort((tagA, tagB) => tagA.tag.localeCompare(tagB.tag));
return tags;
};

View file

@ -0,0 +1,71 @@
import type { FontStyle, FontWeight } from "satori";
export type FontOptions = {
name: string;
data: ArrayBuffer;
weight: FontWeight | undefined;
style: FontStyle | undefined;
};
async function loadGoogleFont(
font: string,
text: string
): Promise<ArrayBuffer> {
const API = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`;
const css = await (
await fetch(API, {
headers: {
"User-Agent":
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1",
},
})
).text();
const resource = css.match(
/src: url\((.+)\) format\('(opentype|truetype)'\)/
);
if (!resource) throw new Error("Failed to download dynamic font");
const res = await fetch(resource[1]);
if (!res.ok) {
throw new Error("Failed to download dynamic font. Status: " + res.status);
}
const fonts: ArrayBuffer = await res.arrayBuffer();
return fonts;
}
async function loadGoogleFonts(
text: string
): Promise<
Array<{ name: string; data: ArrayBuffer; weight: number; style: string }>
> {
const fontsConfig = [
{
name: "IBM Plex Mono",
font: "IBM+Plex+Mono",
weight: 400,
style: "normal",
},
{
name: "IBM Plex Mono",
font: "IBM+Plex+Mono:wght@700",
weight: 700,
style: "bold",
},
];
const fonts = await Promise.all(
fontsConfig.map(async ({ name, font, weight, style }) => {
const data = await loadGoogleFont(font, text);
return { name, data, weight, style };
})
);
return fonts;
}
export default loadGoogleFonts;

View file

@ -1,8 +1,10 @@
import { SITE } from "@config";
import satori from "satori";
import type { CollectionEntry } from "astro:content";
import { SITE } from "@config";
import loadGoogleFonts, { type FontOptions } from "../loadGoogleFont";
export default (post: CollectionEntry<"blog">) => {
return (
export default async (post: CollectionEntry<"blog">) => {
return satori(
<div
style={{
background: "#fefbfb",
@ -91,6 +93,14 @@ export default (post: CollectionEntry<"blog">) => {
</div>
</div>
</div>
</div>
</div>,
{
width: 1200,
height: 630,
embedFont: true,
fonts: (await loadGoogleFonts(
post.data.title + post.data.author + SITE.title + "by"
)) as FontOptions[],
}
);
};

View file

@ -1,7 +1,9 @@
import satori from "satori";
import { SITE } from "@config";
import loadGoogleFonts, { type FontOptions } from "../loadGoogleFont";
export default () => {
return (
export default async () => {
return satori(
<div
style={{
background: "#fefbfb",
@ -82,6 +84,14 @@ export default () => {
</div>
</div>
</div>
</div>
</div>,
{
width: 1200,
height: 630,
embedFont: true,
fonts: (await loadGoogleFonts(
SITE.title + SITE.desc + SITE.website
)) as FontOptions[],
}
);
};

11
src/utils/postFilter.ts Normal file
View file

@ -0,0 +1,11 @@
import { SITE } from "@config";
import type { CollectionEntry } from "astro:content";
const postFilter = ({ data }: CollectionEntry<"blog">) => {
const isPublishTimePassed =
Date.now() >
new Date(data.pubDatetime).getTime() - SITE.scheduledPostMargin;
return !data.draft && (import.meta.env.DEV || isPublishTimePassed);
};
export default postFilter;

View file

@ -1,11 +1,5 @@
import { slug as slugger } from "github-slugger";
import type { CollectionEntry } from "astro:content";
import kebabCase from "lodash.kebabcase";
export const slugifyStr = (str: string) => slugger(str);
const slugify = (post: CollectionEntry<"blog">["data"]) =>
post.postSlug ? slugger(post.postSlug) : slugger(post.title);
export const slugifyStr = (str: string) => kebabCase(str);
export const slugifyAll = (arr: string[]) => arr.map(str => slugifyStr(str));
export default slugify;

View file

@ -9,6 +9,7 @@ function withOpacity(variableName) {
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["selector", "[data-theme='dark']"],
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
// Remove the following screen breakpoint or add other breakpoints
@ -17,10 +18,7 @@ module.exports = {
sm: "640px",
},
// Uncomment the following extend
// if existing Tailwind color palette will be used
// extend: {
extend: {
textColor: {
skin: {
base: withOpacity("--color-text-base"),
@ -56,10 +54,28 @@ module.exports = {
},
transparent: "transparent",
},
stroke: {
skin: {
accent: withOpacity("--color-accent")
}
},
fontFamily: {
mono: ["IBM Plex Mono", "monospace"],
},
// },
typography: {
DEFAULT: {
css: {
pre: {
color: false,
},
code: {
color: false,
},
},
},
},
},
},
plugins: [require("@tailwindcss/typography")],
};