feat: revamped author card
173
package-lock.json
generated
|
@ -47,7 +47,7 @@
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-astro": "^0.13.0",
|
"prettier-plugin-astro": "^0.13.0",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-astro-organize-imports": "^0.4.9",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.14"
|
"prettier-plugin-tailwindcss": "^0.5.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -527,10 +527,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.24.7",
|
"version": "7.24.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
|
||||||
"integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==",
|
"integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
|
@ -2612,6 +2611,129 @@
|
||||||
"tailwindcss": ">=3.0.0 || insiders"
|
"tailwindcss": ">=3.0.0 || insiders"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/generator": "7.17.7",
|
||||||
|
"@babel/parser": "^7.20.5",
|
||||||
|
"@babel/traverse": "7.23.2",
|
||||||
|
"@babel/types": "7.17.0",
|
||||||
|
"javascript-natural-sort": "0.7.1",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/compiler-sfc": "3.x",
|
||||||
|
"prettier": "2.x - 3.x"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/compiler-sfc": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/generator": {
|
||||||
|
"version": "7.17.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz",
|
||||||
|
"integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.17.0",
|
||||||
|
"jsesc": "^2.5.1",
|
||||||
|
"source-map": "^0.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/traverse": {
|
||||||
|
"version": "7.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
|
||||||
|
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.22.13",
|
||||||
|
"@babel/generator": "^7.23.0",
|
||||||
|
"@babel/helper-environment-visitor": "^7.22.20",
|
||||||
|
"@babel/helper-function-name": "^7.23.0",
|
||||||
|
"@babel/helper-hoist-variables": "^7.22.5",
|
||||||
|
"@babel/helper-split-export-declaration": "^7.22.6",
|
||||||
|
"@babel/parser": "^7.23.0",
|
||||||
|
"@babel/types": "^7.23.0",
|
||||||
|
"debug": "^4.1.0",
|
||||||
|
"globals": "^11.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/traverse/node_modules/@babel/generator": {
|
||||||
|
"version": "7.25.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz",
|
||||||
|
"integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.25.6",
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
|
"jsesc": "^2.5.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/traverse/node_modules/@babel/types": {
|
||||||
|
"version": "7.25.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
|
||||||
|
"integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-string-parser": "^7.24.8",
|
||||||
|
"@babel/helper-validator-identifier": "^7.24.7",
|
||||||
|
"to-fast-properties": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/types": {
|
||||||
|
"version": "7.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
|
||||||
|
"integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-validator-identifier": "^7.16.7",
|
||||||
|
"to-fast-properties": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/source-map": {
|
||||||
|
"version": "0.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||||
|
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/acorn": {
|
"node_modules/@types/acorn": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz",
|
||||||
|
@ -5141,6 +5263,14 @@
|
||||||
"@pkgjs/parseargs": "^0.11.0"
|
"@pkgjs/parseargs": "^0.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/javascript-natural-sort": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "1.21.6",
|
"version": "1.21.6",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
|
||||||
|
@ -5318,6 +5448,14 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/lodash.castarray": {
|
"node_modules/lodash.castarray": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||||
|
@ -7331,6 +7469,29 @@
|
||||||
"node": "^14.15.0 || >=16.0.0"
|
"node": "^14.15.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier-plugin-astro-organize-imports": {
|
||||||
|
"version": "0.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier-plugin-astro-organize-imports/-/prettier-plugin-astro-organize-imports-0.4.9.tgz",
|
||||||
|
"integrity": "sha512-iMiTSTOs3YDVDojSUZKIZanovqbonGqX2hxp6XM3l2tWXTyQZodpOSLcK3p2PB2rMVu0K5OgUVv3J+ypIO5Evw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/compiler": "^2.8.0",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"prettier": "^3.0",
|
||||||
|
"prettier-plugin-astro": "*",
|
||||||
|
"prettier-plugin-tailwindcss": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"prettier-plugin-astro": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prettier-plugin-tailwindcss": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler": {
|
"node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler": {
|
||||||
"version": "1.8.2",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-1.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-1.8.2.tgz",
|
||||||
|
@ -7344,6 +7505,8 @@
|
||||||
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
|
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@volar/vue-language-plugin-pug": "^1.0.4",
|
"@volar/vue-language-plugin-pug": "^1.0.4",
|
||||||
"@volar/vue-typescript": "^1.0.4",
|
"@volar/vue-typescript": "^1.0.4",
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-astro": "^0.13.0",
|
"prettier-plugin-astro": "^0.13.0",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-astro-organize-imports": "^0.4.9",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.14"
|
"prettier-plugin-tailwindcss": "^0.5.14"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
@ -59,8 +59,8 @@
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"prettier-plugin-astro",
|
"prettier-plugin-astro",
|
||||||
"prettier-plugin-organize-imports",
|
"prettier-plugin-tailwindcss",
|
||||||
"prettier-plugin-tailwindcss"
|
"prettier-plugin-astro-organize-imports"
|
||||||
],
|
],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
|
Before Width: | Height: | Size: 856 B After Width: | Height: | Size: 856 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 838 B After Width: | Height: | Size: 838 B |
|
@ -2,7 +2,7 @@
|
||||||
<browserconfig>
|
<browserconfig>
|
||||||
<msapplication>
|
<msapplication>
|
||||||
<tile>
|
<tile>
|
||||||
<square150x150logo src="/favicons/mstile-150x150.png"/>
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
<TileColor>#da532c</TileColor>
|
<TileColor>#da532c</TileColor>
|
||||||
</tile>
|
</tile>
|
||||||
</msapplication>
|
</msapplication>
|
Before Width: | Height: | Size: 495 B After Width: | Height: | Size: 495 B |
Before Width: | Height: | Size: 598 B After Width: | Height: | Size: 598 B |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 877 B After Width: | Height: | Size: 877 B |
|
@ -3,12 +3,12 @@
|
||||||
"short_name": "",
|
"short_name": "",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/favicons/android-chrome-192x192.png",
|
"src": "/android-chrome-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/favicons/android-chrome-512x512.png",
|
"src": "/android-chrome-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
Before Width: | Height: | Size: 2.2 KiB |
|
@ -1,30 +1,110 @@
|
||||||
---
|
---
|
||||||
import type { CollectionEntry } from 'astro:content'
|
|
||||||
import Link from '@components/Link.astro'
|
|
||||||
import AvatarComponent from '@/components/ui/avatar'
|
import AvatarComponent from '@/components/ui/avatar'
|
||||||
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import Link from '@components/Link.astro'
|
||||||
|
import type { CollectionEntry } from 'astro:content'
|
||||||
|
import { Github, Globe, Linkedin, Twitter } from 'lucide-react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
author: CollectionEntry<'authors'>
|
author: CollectionEntry<'authors'>
|
||||||
|
linkDisabled?: boolean
|
||||||
}
|
}
|
||||||
|
const { author, linkDisabled = false } = Astro.props
|
||||||
const { author } = Astro.props
|
const { name, avatar, bio, pronouns, github, twitter, linkedin, website } =
|
||||||
const { name, avatar, bio } = author.data
|
author.data
|
||||||
---
|
---
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="rounded-xl border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50"
|
class="rounded-xl border p-4 transition-colors duration-300 ease-in-out has-[a:hover]:bg-secondary/50"
|
||||||
>
|
>
|
||||||
<Link href={`/authors/${author.slug}`} class="flex flex-wrap gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<Link
|
||||||
|
href={`/authors/${author.slug}`}
|
||||||
|
class={cn('block', linkDisabled && 'pointer-events-none')}
|
||||||
|
>
|
||||||
<AvatarComponent
|
<AvatarComponent
|
||||||
client:load
|
client:load
|
||||||
src={avatar}
|
src={avatar}
|
||||||
alt={`Avatar of ${name}`}
|
alt={`Avatar of ${name}`}
|
||||||
fallback={name[0]}
|
fallback={name[0]}
|
||||||
className="size-32 rounded-md"
|
className={cn(
|
||||||
|
'size-32 rounded-md',
|
||||||
|
!linkDisabled &&
|
||||||
|
'transition-shadow duration-300 hover:cursor-pointer hover:ring-2 hover:ring-primary',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div class="flex-grow">
|
</Link>
|
||||||
<h3 class="mb-1 text-lg font-semibold">{name}</h3>
|
<div class="flex flex-grow flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h3 class="text-lg font-semibold">{name}</h3>
|
||||||
|
{
|
||||||
|
pronouns && (
|
||||||
|
<span class="text-sm text-muted-foreground">({pronouns})</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<p class="text-sm text-muted-foreground">{bio}</p>
|
<p class="text-sm text-muted-foreground">{bio}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ul class="flex gap-2">
|
||||||
|
{
|
||||||
|
github && (
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href={`https://github.com/${github}`}
|
||||||
|
aria-label="GitHub"
|
||||||
|
title="GitHub"
|
||||||
|
class={buttonVariants({ variant: 'outline', size: 'icon' })}
|
||||||
|
>
|
||||||
|
<Github className="size-4" />
|
||||||
</Link>
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
twitter && (
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href={`https://twitter.com/${twitter}`}
|
||||||
|
aria-label="Twitter"
|
||||||
|
title="Twitter"
|
||||||
|
class={buttonVariants({ variant: 'outline', size: 'icon' })}
|
||||||
|
>
|
||||||
|
<Twitter className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
linkedin && (
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href={`https://linkedin.com/in/${linkedin}`}
|
||||||
|
aria-label="LinkedIn"
|
||||||
|
title="LinkedIn"
|
||||||
|
class={buttonVariants({ variant: 'outline', size: 'icon' })}
|
||||||
|
>
|
||||||
|
<Linkedin className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
website && (
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href={website}
|
||||||
|
aria-label="Website"
|
||||||
|
title="Website"
|
||||||
|
class={buttonVariants({ variant: 'outline', size: 'icon' })}
|
||||||
|
>
|
||||||
|
<Globe className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
import type { CollectionEntry } from 'astro:content'
|
import AvatarComponent from '@/components/ui/avatar'
|
||||||
import { formatDate, readingTime, parseAuthors } from '@lib/utils'
|
|
||||||
import { Image } from 'astro:assets'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import AvatarComponent from '@/components/ui/avatar'
|
import { formatDate, parseAuthors, readingTime } from '@lib/utils'
|
||||||
|
import { Image } from 'astro:assets'
|
||||||
|
import type { CollectionEntry } from 'astro:content'
|
||||||
import Link from './Link.astro'
|
import Link from './Link.astro'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
53
src/components/Breadcrumbs.astro
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
---
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from '@/components/ui/breadcrumb'
|
||||||
|
import { HomeIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export interface BreadcrumbItem {
|
||||||
|
href?: string
|
||||||
|
label: string
|
||||||
|
icon?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: BreadcrumbItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { items } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink href="/">
|
||||||
|
<HomeIcon className="size-4" />
|
||||||
|
</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
{
|
||||||
|
items.map((item, index) => (
|
||||||
|
<>
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
{index === items.length - 1 ? (
|
||||||
|
<BreadcrumbPage>
|
||||||
|
{item.icon && <item.icon className="size-4 mr-1" />}
|
||||||
|
{item.label}
|
||||||
|
</BreadcrumbPage>
|
||||||
|
) : (
|
||||||
|
<BreadcrumbLink href={item.href}>
|
||||||
|
{item.icon && <item.icon className="size-4 mr-1" />}
|
||||||
|
{item.label}
|
||||||
|
</BreadcrumbLink>
|
||||||
|
)}
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
|
@ -2,8 +2,8 @@
|
||||||
import '../styles/global.css'
|
import '../styles/global.css'
|
||||||
import '../styles/katex.css'
|
import '../styles/katex.css'
|
||||||
|
|
||||||
import '@fontsource/geist-sans'
|
|
||||||
import '@fontsource/geist-mono'
|
import '@fontsource/geist-mono'
|
||||||
|
import '@fontsource/geist-sans'
|
||||||
|
|
||||||
import { ViewTransitions } from 'astro:transitions'
|
import { ViewTransitions } from 'astro:transitions'
|
||||||
|
|
||||||
|
@ -28,28 +28,14 @@ const { title, description, image = '/static/twitter-card.png' } = Astro.props
|
||||||
<meta name="title" content={title} />
|
<meta name="title" content={title} />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
|
|
||||||
<link
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
rel="apple-touch-icon"
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
sizes="180x180"
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
href="/favicons/apple-touch-icon.png"
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
/>
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||||
<link
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="32x32"
|
|
||||||
href="/favicons/favicon-32x32.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="16x16"
|
|
||||||
href="/favicons/favicon-16x16.png"
|
|
||||||
/>
|
|
||||||
<link rel="manifest" href="/favicons/site.webmanifest" />
|
|
||||||
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5" />
|
|
||||||
<link rel="shortcut icon" href="/favicons/favicon.ico" />
|
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
<meta name="msapplication-config" content="/favicons/browserconfig.xml" />
|
<meta name="msapplication-config" content="/browserconfig.xml" />
|
||||||
<meta name="theme-color" content="#121212" />
|
<meta name="theme-color" content="#121212" />
|
||||||
|
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
---
|
---
|
||||||
|
import { ModeToggle } from '@/components/ui/mode-toggle'
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
import Link from '@components/Link.astro'
|
import Link from '@components/Link.astro'
|
||||||
import { SITE } from '@consts'
|
import { NAV_LINKS, SITE } from '@consts'
|
||||||
import { ModeToggle } from '@/components/ui/mode-toggle'
|
|
||||||
import { Image } from 'astro:assets'
|
import { Image } from 'astro:assets'
|
||||||
import logo from '../../public/static/logo.svg'
|
import logo from '../../public/static/logo.svg'
|
||||||
|
|
||||||
const items = [
|
|
||||||
{ href: '/blog', label: 'blog' },
|
|
||||||
{ href: '/authors', label: 'authors' },
|
|
||||||
{ href: '/about', label: 'about' },
|
|
||||||
]
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<header
|
<header
|
||||||
|
@ -29,7 +23,7 @@ const items = [
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<nav class="flex items-center gap-4 text-sm sm:gap-6">
|
<nav class="flex items-center gap-4 text-sm sm:gap-6">
|
||||||
{
|
{
|
||||||
items.map((item) => (
|
NAV_LINKS.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={item.href}
|
||||||
class="capitalize text-foreground/60 transition-colors hover:text-foreground/80"
|
class="capitalize text-foreground/60 transition-colors hover:text-foreground/80"
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
---
|
---
|
||||||
import Link from '@components/Link.astro'
|
|
||||||
import { ArrowLeft, ArrowRight } from 'lucide-react'
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import Link from '@components/Link.astro'
|
||||||
|
import { ArrowLeft, ArrowRight } from 'lucide-react'
|
||||||
|
|
||||||
const { prevPost, nextPost } = Astro.props
|
const { prevPost, nextPost } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="mt-8 flex justify-between gap-4">
|
<div class="flex justify-between gap-4">
|
||||||
<Link
|
<Link
|
||||||
href={prevPost ? `/blog/${prevPost.slug}` : '#'}
|
href={prevPost ? `/blog/${prevPost.slug}` : '#'}
|
||||||
class={cn(
|
class={cn(
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
import { Twitter, Github, Linkedin, Mail, Rss } from 'lucide-react'
|
|
||||||
import { SITE } from '@consts'
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import Link from '@components/Link.astro'
|
import Link from '@components/Link.astro'
|
||||||
|
import { SITE } from '@consts'
|
||||||
|
import { Github, Linkedin, Mail, Rss, Twitter } from 'lucide-react'
|
||||||
---
|
---
|
||||||
|
|
||||||
<ul class="not-prose flex flex-wrap gap-2" role="list">
|
<ul class="not-prose flex flex-wrap gap-2" role="list">
|
||||||
|
|
|
@ -27,7 +27,7 @@ function buildToc(headings: Heading[]) {
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<details open class="group mb-8 block rounded-xl border p-4 xl:hidden">
|
<details open class="group block rounded-xl border p-4 xl:hidden">
|
||||||
<summary
|
<summary
|
||||||
class="flex cursor-pointer items-center justify-between text-xl font-semibold"
|
class="flex cursor-pointer items-center justify-between text-xl font-semibold"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import type { Heading } from './TableOfContents.astro'
|
|
||||||
import Link from './Link.astro'
|
import Link from './Link.astro'
|
||||||
|
import type { Heading } from './TableOfContents.astro'
|
||||||
|
|
||||||
const { heading } = Astro.props
|
const { heading } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const Avatar = React.forwardRef<
|
const Avatar = React.forwardRef<
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { type VariantProps, cva } from 'class-variance-authority'
|
||||||
import { Hash } from 'lucide-react'
|
import { Hash } from 'lucide-react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring focus:ring-ring',
|
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring focus:ring-ring',
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons'
|
import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons'
|
||||||
import { Slot } from '@radix-ui/react-slot'
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const Breadcrumb = React.forwardRef<
|
const Breadcrumb = React.forwardRef<
|
||||||
HTMLElement,
|
HTMLElement,
|
||||||
React.ComponentPropsWithoutRef<'nav'> & {
|
React.ComponentPropsWithoutRef<'nav'> & {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { Slot } from '@radix-ui/react-slot'
|
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
|
import { type VariantProps, cva } from 'class-variance-authority'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
const Card = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
@ -6,8 +7,6 @@ import {
|
||||||
} from '@radix-ui/react-icons'
|
} from '@radix-ui/react-icons'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
@ -49,7 +48,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -67,7 +66,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import { Laptop, Moon, Sun } from 'lucide-react'
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
@ -8,6 +5,8 @@ import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { Laptop, Moon, Sun } from 'lucide-react'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const [theme, setThemeState] = React.useState<
|
const [theme, setThemeState] = React.useState<
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
const Separator = React.forwardRef<
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
|
|
|
@ -6,11 +6,22 @@ export type Site = {
|
||||||
SITEURL: string
|
SITEURL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NavigationLink = {
|
||||||
|
href: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
export const SITE: Site = {
|
export const SITE: Site = {
|
||||||
TITLE: 'astro-erudite',
|
TITLE: 'astro-erudite',
|
||||||
DESCRIPTION:
|
DESCRIPTION:
|
||||||
'astro-erudite is a opinionated, no-frills blogging template. Built with Astro.',
|
'astro-erudite is a opinionated, no-frills blogging template. Built with Astro.',
|
||||||
EMAIL: 'youremail@gmail.com',
|
EMAIL: 'jason@enscribe.dev',
|
||||||
NUM_POSTS_ON_HOMEPAGE: 2,
|
NUM_POSTS_ON_HOMEPAGE: 2,
|
||||||
SITEURL: 'https://astro-erudite.vercel.app',
|
SITEURL: 'https://astro-erudite.vercel.app',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NAV_LINKS: NavigationLink[] = [
|
||||||
|
{ href: '/blog', label: 'blog' },
|
||||||
|
{ href: '/authors', label: 'authors' },
|
||||||
|
{ href: '/about', label: 'about' },
|
||||||
|
]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
---
|
---
|
||||||
name: 'enscribe'
|
name: 'enscribe'
|
||||||
|
pronouns: 'he/him'
|
||||||
avatar: 'https://gravatar.com/avatar/9bfdc4ec972793cf05cb91efce5f4aaaec2a0da1bf4ec34dad0913f1d845faf6.webp?size=256'
|
avatar: 'https://gravatar.com/avatar/9bfdc4ec972793cf05cb91efce5f4aaaec2a0da1bf4ec34dad0913f1d845faf6.webp?size=256'
|
||||||
bio: 'd(-_-)b'
|
bio: 'd(-_-)b'
|
||||||
website: 'https://enscribe.dev'
|
website: 'https://enscribe.dev'
|
||||||
|
|
|
@ -9,7 +9,7 @@ authors: ['enscribe']
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Hello! My name is enscribe, and I'm a fullstack web developer who has been fiddling with blogging platforms for a couple of years now. I run a blog at [enscribe.dev](https://enscribe.dev), where I write about cybersecurity and the capture-the-flag (CTF) scene.
|
Hello! My name is enscribe ([jktrn](https://github.com/jktrn) on GitHub), and I'm a fullstack web developer who has been fiddling with blogging platforms for a couple of years now. I run a blog at [enscribe.dev](https://enscribe.dev), where I write about cybersecurity and the capture-the-flag (CTF) scene.
|
||||||
|
|
||||||
I have a lot of opinions about what makes a great blogging template. As a cumulative result of all the slop, bullshit, and outright terrible design decisions I've had to deal with working with various templates and frameworks, I bring you [astro-erudite](https://github.com/jktrn/astro-erudite), which should hopefully bring a better developer and user experience in terms of ease of use, customization, and performance.
|
I have a lot of opinions about what makes a great blogging template. As a cumulative result of all the slop, bullshit, and outright terrible design decisions I've had to deal with working with various templates and frameworks, I bring you [astro-erudite](https://github.com/jktrn/astro-erudite), which should hopefully bring a better developer and user experience in terms of ease of use, customization, and performance.
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ astro-erudite is written in Astro, a framework hyperoptimized for static content
|
||||||
|
|
||||||
This is a non-exhaustive list of features I believe are essential for a frictionless developer experience:
|
This is a non-exhaustive list of features I believe are essential for a frictionless developer experience:
|
||||||
|
|
||||||
- [shadcn/ui](https://ui.shadcn.com) is a pretty controversial component library. I love it. I don't care much for the components themselves as they are literally [Radix](https://www.radix-ui.com/) primitive wrappers. However, the best part is arguably its take on [theming](https://ui.shadcn.com/docs/theming), which introduces a convention involving CSS colors such as `background` and `foreground` into your Tailwind configuration so that styling is a breeze. These classes also automatically adapt to the user's selected theme, and as such you don't need to worry about adding an equivalent `dark:` style to all of your theming. shadcn/ui turns `"bg-stone-50 text-stone-900 dark:bg-stone-900 dark:text-stone-50"` into `"bg-background text-foreground"`, both more semantic and easier to blanket edit (if you wanted to change all your blues in your site to indigos, you would need to go around every single class and change it rather than editing a single CSS variable). Other utiliy colors such as `secondary`, `muted`, `accent`, and `destructive` also exist and are very self-explanatory in name (and also have an equivalent `-foreground` class, e.g. `secondary-foreground`, which you can apply to text on top of these colors).
|
- [shadcn/ui](https://ui.shadcn.com) is a pretty controversial component library. I love it. I don't care much for the components themselves as they are literally [Radix](https://www.radix-ui.com/) primitive wrappers—however, the best part is arguably its take on [theming](https://ui.shadcn.com/docs/theming), which introduces a convention involving CSS colors such as `background` and `foreground` into your Tailwind configuration so that styling is a breeze. These classes also automatically adapt to the user's selected theme, and as such you don't need to worry about adding an equivalent `dark:` style to all of your theming. shadcn/ui turns `"bg-stone-50 text-stone-900 dark:bg-stone-900 dark:text-stone-50"` into `"bg-background text-foreground"`, both more semantic and easier to blanket edit (if you wanted to change all your blues in your site to indigos, you would need to go around every single class and change it rather than editing a single CSS variable). Other utiliy colors such as `secondary`, `muted`, `accent`, and `destructive` also exist and are very self-explanatory in name (and also have an equivalent `-foreground` class, e.g. `secondary-foreground`, which you can apply to text on top of these colors).
|
||||||
- [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) is a plugin that automatically styles any content surrounded by an `<article>{:html}` tag in a way which makes it readable and blog-post-friendly. It does this via a `prose` class which you can wrap anything with to style the interior content. This is especially useful for HTML you don't control, e.g. a post rendered from Markdown. Although your control over the rendering is a bit less fine-grained, you're also already using Tailwind so this right has long been forsaken.
|
- [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) is a plugin that automatically styles any content surrounded by an `<article>{:html}` tag in a way which makes it readable and blog-post-friendly. It does this via a `prose` class which you can wrap anything with to style the interior content. This is especially useful for HTML you don't control, e.g. a post rendered from Markdown. Although your control over the rendering is a bit less fine-grained, you're also already using Tailwind so this right has long been forsaken.
|
||||||
- [Shiki](https://github.com/shikijs/shiki) is a syntax highlighter for code blocks. Although Astro code blocks utilize Shiki under the hood, I've actually disabled the default code blocks in this template so that they don't collide with my preferred library [rehype-pretty-code](https://rehype-pretty.pages.dev), which is _also_ powered by Shiki but allows for line numbers, line highlighting, inline code snytax highlighting, and a transformers API for advanced customization such as manual `diff` visualization and line blurring. This library does not ship with any CSS, and it's up to you to style the code blocks and code block titles as you see fit. I've provided styles in `src/styles/global.css` within the `@layer components{:css}` directive if you wish to fiddle with them. The following code block is an example of how to style code blocks using rehype-pretty-code, and was generated with the following Markdown code:
|
- [Shiki](https://github.com/shikijs/shiki) is a syntax highlighter for code blocks. Although Astro code blocks utilize Shiki under the hood, I've actually disabled the default code blocks in this template so that they don't collide with my preferred library [rehype-pretty-code](https://rehype-pretty.pages.dev), which is _also_ powered by Shiki but allows for line numbers, line highlighting, inline code snytax highlighting, and a transformers API for advanced customization such as manual `diff` visualization and line blurring. This library does not ship with any CSS, and it's up to you to style the code blocks and code block titles as you see fit. I've provided styles in `src/styles/global.css` within the `@layer components{:css}` directive if you wish to fiddle with them. The following code block is an example of how to style code blocks using rehype-pretty-code, and was generated with the following Markdown code:
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ This is a non-exhaustive list of features I believe are essential for a friction
|
||||||
- The `cn(){:ts}` function is a utility function which combines [clsx](https://www.npmjs.com/package/clsx) and [tailwind-merge](https://www.npmjs.com/package/tailwind-merge), two packages which allow painless conditional class addition and concatenation:
|
- The `cn(){:ts}` function is a utility function which combines [clsx](https://www.npmjs.com/package/clsx) and [tailwind-merge](https://www.npmjs.com/package/tailwind-merge), two packages which allow painless conditional class addition and concatenation:
|
||||||
|
|
||||||
```tsx title="src/lib/utils.ts" caption="A utility function for class name concatenation" showLineNumbers
|
```tsx title="src/lib/utils.ts" caption="A utility function for class name concatenation" showLineNumbers
|
||||||
import { clsx, type ClassValue } from 'clsx'
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
|
|
@ -17,6 +17,7 @@ const authors = defineCollection({
|
||||||
type: 'content',
|
type: 'content',
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
pronouns: z.string().optional(),
|
||||||
avatar: z.string().url(),
|
avatar: z.string().url(),
|
||||||
bio: z.string().optional(),
|
bio: z.string().optional(),
|
||||||
website: z.string().url().optional(),
|
website: z.string().url().optional(),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
|
import Footer from '@components/Footer.astro'
|
||||||
import Head from '@components/Head.astro'
|
import Head from '@components/Head.astro'
|
||||||
import Header from '@components/Header.astro'
|
import Header from '@components/Header.astro'
|
||||||
import Footer from '@components/Footer.astro'
|
|
||||||
import { SITE } from '@consts'
|
import { SITE } from '@consts'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -24,7 +24,7 @@ const { title, description, image } = Astro.props
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div
|
<div
|
||||||
class="box-border flex h-fit min-h-screen flex-col gap-y-6 bg-background px-4 font-sans text-foreground antialiased"
|
class="box-border flex h-fit min-h-screen flex-col gap-y-6 px-4 font-sans antialiased"
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<main class="flex-grow">
|
<main class="flex-grow">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getEntry } from 'astro:content'
|
import { getEntry } from 'astro:content'
|
||||||
import { clsx, type ClassValue } from 'clsx'
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
@ -29,14 +29,14 @@ export async function parseAuthors(authors: string[]) {
|
||||||
const author = await getEntry('authors', slug)
|
const author = await getEntry('authors', slug)
|
||||||
return {
|
return {
|
||||||
name: author?.data?.name || slug,
|
name: author?.data?.name || slug,
|
||||||
avatar: author?.data?.avatar || '/static/512x512.png',
|
avatar: author?.data?.avatar || '/static/logo.png',
|
||||||
isRegistered: !!author,
|
isRegistered: !!author,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching author with slug ${slug}:`, error)
|
console.error(`Error fetching author with slug ${slug}:`, error)
|
||||||
return {
|
return {
|
||||||
name: slug,
|
name: slug,
|
||||||
avatar: '/static/512x512.png',
|
avatar: '/static/logo.png',
|
||||||
isRegistered: false,
|
isRegistered: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,16 @@
|
||||||
---
|
---
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||||
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
import Link from '@components/Link.astro'
|
import Link from '@components/Link.astro'
|
||||||
import { SITE } from '@consts'
|
import { SITE } from '@consts'
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from '@/components/ui/breadcrumb'
|
|
||||||
import { HomeIcon } from 'lucide-react'
|
|
||||||
import { cn } from '@lib/utils'
|
import { cn } from '@lib/utils'
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="404" description={SITE.DESCRIPTION}>
|
<Layout title="404" description={SITE.DESCRIPTION}>
|
||||||
<Container class="flex grow flex-col gap-y-6">
|
<Container class="flex grow flex-col gap-y-6">
|
||||||
<Breadcrumb>
|
<Breadcrumbs items={[{ label: '???' }]} />
|
||||||
<BreadcrumbList>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbLink href="/"
|
|
||||||
><HomeIcon className="size-4" /></BreadcrumbLink
|
|
||||||
>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>???</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class="flex flex-col items-center justify-center gap-y-4 text-center"
|
class="flex flex-col items-center justify-center gap-y-4 text-center"
|
||||||
|
|
|
@ -1,34 +1,13 @@
|
||||||
---
|
---
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
import Link from '@components/Link.astro'
|
|
||||||
import { SITE } from '@consts'
|
import { SITE } from '@consts'
|
||||||
import {
|
import Layout from '@layouts/Layout.astro'
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from '@/components/ui/breadcrumb'
|
|
||||||
import { HomeIcon } from 'lucide-react'
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="About" description={SITE.DESCRIPTION}>
|
<Layout title="About" description={SITE.DESCRIPTION}>
|
||||||
<Container class="flex flex-col gap-y-6">
|
<Container class="flex flex-col gap-y-6">
|
||||||
<Breadcrumb>
|
<Breadcrumbs items={[{ label: 'About' }]} />
|
||||||
<BreadcrumbList>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbLink href="/"
|
|
||||||
><HomeIcon className="size-4" /></BreadcrumbLink
|
|
||||||
>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>About</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="min-w-full">
|
<div class="min-w-full">
|
||||||
|
|
|
@ -1,19 +1,10 @@
|
||||||
---
|
---
|
||||||
import { type CollectionEntry, getCollection } from 'astro:content'
|
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||||
import Layout from '@layouts/Layout.astro'
|
|
||||||
import Container from '@components/Container.astro'
|
|
||||||
import AuthorCard from '@components/AuthorCard.astro'
|
import AuthorCard from '@components/AuthorCard.astro'
|
||||||
import BlogCard from '@components/BlogCard.astro'
|
import BlogCard from '@components/BlogCard.astro'
|
||||||
import { Button } from '@/components/ui/button'
|
import Container from '@components/Container.astro'
|
||||||
import {
|
import Layout from '@layouts/Layout.astro'
|
||||||
Breadcrumb,
|
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from '@/components/ui/breadcrumb'
|
|
||||||
import { HomeIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const authors = await getCollection('authors')
|
const authors = await getCollection('authors')
|
||||||
|
@ -38,33 +29,21 @@ const authorPosts = allPosts
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
title={`${author.data.name} - Author`}
|
title={`${author.data.name} (Author)`}
|
||||||
description={author.data.bio || `Profile of ${author.data.name}.`}
|
description={author.data.bio || `Profile of ${author.data.name}.`}
|
||||||
>
|
>
|
||||||
<Container>
|
<Container class="flex flex-col gap-y-6">
|
||||||
<Breadcrumb className="mb-6">
|
<Breadcrumbs
|
||||||
<BreadcrumbList>
|
items={[
|
||||||
<BreadcrumbItem>
|
{ href: '/authors', label: 'Authors' },
|
||||||
<BreadcrumbLink href="/"
|
{ label: author.data.name },
|
||||||
><HomeIcon className="size-4" /></BreadcrumbLink
|
]}
|
||||||
>
|
/>
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbLink href="/authors">Authors</BreadcrumbLink>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>{author.data.name}</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
|
|
||||||
<div class="space-y-10">
|
|
||||||
<section>
|
<section>
|
||||||
<AuthorCard author={author} />
|
<AuthorCard author={author} linkDisabled />
|
||||||
</section>
|
</section>
|
||||||
<section class="space-y-4">
|
<section class="flex flex-col gap-y-4">
|
||||||
<h2 class="text-2xl font-semibold">Posts by {author.data.name}</h2>
|
<h2 class="text-2xl font-semibold">Posts by {author.data.name}</h2>
|
||||||
{
|
{
|
||||||
authorPosts.length > 0 ? (
|
authorPosts.length > 0 ? (
|
||||||
|
@ -82,6 +61,5 @@ const authorPosts = allPosts
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,36 +1,16 @@
|
||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content'
|
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||||
import Layout from '@layouts/Layout.astro'
|
|
||||||
import Container from '@components/Container.astro'
|
|
||||||
import AuthorCard from '@components/AuthorCard.astro'
|
import AuthorCard from '@components/AuthorCard.astro'
|
||||||
import {
|
import Container from '@components/Container.astro'
|
||||||
Breadcrumb,
|
import Layout from '@layouts/Layout.astro'
|
||||||
BreadcrumbItem,
|
import { getCollection } from 'astro:content'
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from '@/components/ui/breadcrumb'
|
|
||||||
import { HomeIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
const authors = await getCollection('authors')
|
const authors = await getCollection('authors')
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Authors" description="A list of authors on this site.">
|
<Layout title="Authors" description="A list of authors on this site.">
|
||||||
<Container>
|
<Container class="flex flex-col gap-y-6">
|
||||||
<Breadcrumb className="mb-6">
|
<Breadcrumbs items={[{ label: 'Authors' }]} />
|
||||||
<BreadcrumbList>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbLink href="/"
|
|
||||||
><HomeIcon className="size-4" /></BreadcrumbLink
|
|
||||||
>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>Authors</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
<ul class="not-prose flex flex-col gap-4">
|
<ul class="not-prose flex flex-col gap-4">
|
||||||
{
|
{
|
||||||
authors.map((author) => (
|
authors.map((author) => (
|
||||||
|
|
|
@ -1,25 +1,17 @@
|
||||||
---
|
---
|
||||||
import { type CollectionEntry, getCollection } from 'astro:content'
|
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||||
import Layout from '@layouts/Layout.astro'
|
|
||||||
import Container from '@components/Container.astro'
|
|
||||||
import { formatDate, readingTime } from '@lib/utils'
|
|
||||||
import PostNavigation from '@components/PostNavigation.astro'
|
|
||||||
import TableOfContents from '@components/TableOfContents.astro'
|
|
||||||
import { Image } from 'astro:assets'
|
|
||||||
import { badgeVariants } from '@/components/ui/badge'
|
import { badgeVariants } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from '@/components/ui/breadcrumb'
|
|
||||||
import { Hash, HomeIcon, ArrowUp } from 'lucide-react'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import Container from '@components/Container.astro'
|
||||||
import Link from '@components/Link.astro'
|
import Link from '@components/Link.astro'
|
||||||
import { parseAuthors } from '@lib/utils'
|
import PostNavigation from '@components/PostNavigation.astro'
|
||||||
|
import TableOfContents from '@components/TableOfContents.astro'
|
||||||
|
import Layout from '@layouts/Layout.astro'
|
||||||
|
import { formatDate, parseAuthors, readingTime } from '@lib/utils'
|
||||||
|
import { Image } from 'astro:assets'
|
||||||
|
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||||
|
import { ArrowUp, Hash } from 'lucide-react'
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = (await getCollection('blog'))
|
const posts = (await getCollection('blog'))
|
||||||
|
@ -67,24 +59,10 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
||||||
description={post.data.description}
|
description={post.data.description}
|
||||||
image={post.data.image ?? '/static/1200x630.png'}
|
image={post.data.image ?? '/static/1200x630.png'}
|
||||||
>
|
>
|
||||||
<Container>
|
<Container class="flex flex-col gap-y-6">
|
||||||
<Breadcrumb className="mb-6">
|
<Breadcrumbs
|
||||||
<BreadcrumbList>
|
items={[{ href: '/blog', label: 'Blog' }, { label: post.data.title }]}
|
||||||
<BreadcrumbItem>
|
/>
|
||||||
<BreadcrumbLink href="/"
|
|
||||||
><HomeIcon className="size-4" /></BreadcrumbLink
|
|
||||||
>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbLink href="/blog">Blog</BreadcrumbLink>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>{post.data.title}</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
|
|
||||||
{
|
{
|
||||||
post.data.image && (
|
post.data.image && (
|
||||||
|
@ -93,12 +71,13 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
||||||
alt={post.data.title}
|
alt={post.data.title}
|
||||||
width={1200}
|
width={1200}
|
||||||
height={630}
|
height={630}
|
||||||
class="mb-8 rounded-xl object-cover"
|
class="rounded-xl object-cover"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="mb-8 text-center">
|
<section class="flex flex-col gap-6 text-center">
|
||||||
|
<div>
|
||||||
<h1 class="mb-4 text-4xl font-bold leading-tight sm:text-5xl">
|
<h1 class="mb-4 text-4xl font-bold leading-tight sm:text-5xl">
|
||||||
{post.data.title}
|
{post.data.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -156,10 +135,13 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
||||||
</a>
|
</a>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<span class="text-sm text-muted-foreground">No tags available</span>
|
<span class="text-sm text-muted-foreground">
|
||||||
|
No tags available
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
|
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
---
|
---
|
||||||
import { type CollectionEntry, getCollection } from 'astro:content'
|
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||||
import Layout from '@layouts/Layout.astro'
|
|
||||||
import Container from '@components/Container.astro'
|
|
||||||
import BlogCard from '@components/BlogCard.astro'
|
import BlogCard from '@components/BlogCard.astro'
|
||||||
import {
|
import Container from '@components/Container.astro'
|
||||||
Breadcrumb,
|
import Layout from '@layouts/Layout.astro'
|
||||||
BreadcrumbItem,
|
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from '@/components/ui/breadcrumb'
|
|
||||||
import { HomeIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
const data = (await getCollection('blog'))
|
const data = (await getCollection('blog'))
|
||||||
.filter((post) => !post.data.draft)
|
.filter((post) => !post.data.draft)
|
||||||
|
@ -35,19 +27,7 @@ const years = Object.keys(posts).sort((a, b) => parseInt(b) - parseInt(a))
|
||||||
|
|
||||||
<Layout title="Blog" description="Blog">
|
<Layout title="Blog" description="Blog">
|
||||||
<Container class="flex flex-col gap-y-6">
|
<Container class="flex flex-col gap-y-6">
|
||||||
<Breadcrumb>
|
<Breadcrumbs items={[{ label: 'Blog' }]} />
|
||||||
<BreadcrumbList>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbLink href="/"
|
|
||||||
><HomeIcon className="size-4" /></BreadcrumbLink
|
|
||||||
>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>Blog</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-y-8">
|
<div class="flex flex-col gap-y-8">
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
---
|
---
|
||||||
import Layout from '@layouts/Layout.astro'
|
|
||||||
import Container from '@components/Container.astro'
|
|
||||||
import { SITE } from '@consts'
|
|
||||||
import BlogCard from '@components/BlogCard.astro'
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import { getCollection } from 'astro:content'
|
|
||||||
import Link from '@components/Link.astro'
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
@ -13,6 +7,12 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
|
import BlogCard from '@components/BlogCard.astro'
|
||||||
|
import Container from '@components/Container.astro'
|
||||||
|
import Link from '@components/Link.astro'
|
||||||
|
import { SITE } from '@consts'
|
||||||
|
import Layout from '@layouts/Layout.astro'
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
const blog = (await getCollection('blog'))
|
const blog = (await getCollection('blog'))
|
||||||
.filter((post) => !post.data.draft)
|
.filter((post) => !post.data.draft)
|
||||||
|
@ -20,7 +20,7 @@ const blog = (await getCollection('blog'))
|
||||||
.slice(0, SITE.NUM_POSTS_ON_HOMEPAGE)
|
.slice(0, SITE.NUM_POSTS_ON_HOMEPAGE)
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={SITE.TITLE} description={SITE.DESCRIPTION}>
|
<Layout title="Home" description={SITE.DESCRIPTION}>
|
||||||
<Container class="flex flex-col gap-y-6">
|
<Container class="flex flex-col gap-y-6">
|
||||||
<section>
|
<section>
|
||||||
<Card>
|
<Card>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
---
|
---
|
||||||
import { type CollectionEntry, getCollection } from 'astro:content'
|
|
||||||
import Layout from '@layouts/Layout.astro'
|
|
||||||
import Container from '@components/Container.astro'
|
|
||||||
import BlogCard from '@components/BlogCard.astro'
|
import BlogCard from '@components/BlogCard.astro'
|
||||||
|
import Breadcrumbs from '@components/Breadcrumbs.astro'
|
||||||
|
import Container from '@components/Container.astro'
|
||||||
|
import Layout from '@layouts/Layout.astro'
|
||||||
|
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||||
import { Hash } from 'lucide-react'
|
import { Hash } from 'lucide-react'
|
||||||
|
|
||||||
type BlogPost = CollectionEntry<'blog'>
|
type BlogPost = CollectionEntry<'blog'>
|
||||||
|
@ -35,8 +36,8 @@ export async function getStaticPaths() {
|
||||||
title={`Posts tagged with "${tag}"`}
|
title={`Posts tagged with "${tag}"`}
|
||||||
description={`A collection of posts tagged with ${tag}.`}
|
description={`A collection of posts tagged with ${tag}.`}
|
||||||
>
|
>
|
||||||
<Container>
|
<Container class="flex flex-col gap-y-6">
|
||||||
<div class="space-y-10">
|
<Breadcrumbs items={[{ href: '/tags', label: 'Tags' }, { label: tag }]} />
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1 class="text-3xl font-semibold">Posts tagged with</h1>
|
<h1 class="text-3xl font-semibold">Posts tagged with</h1>
|
||||||
<span
|
<span
|
||||||
|
@ -45,10 +46,10 @@ export async function getStaticPaths() {
|
||||||
<Hash className="size-6" />{tag}
|
<Hash className="size-6" />{tag}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="flex flex-col gap-y-4">
|
||||||
{
|
{
|
||||||
posts.map((post) => (
|
posts.map((post) => (
|
||||||
<section class="space-y-4">
|
<section class="flex flex-col gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<ul class="not-prose flex flex-col gap-4">
|
<ul class="not-prose flex flex-col gap-4">
|
||||||
<li>
|
<li>
|
||||||
|
@ -60,6 +61,5 @@ export async function getStaticPaths() {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content'
|
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||||
import Layout from '@layouts/Layout.astro'
|
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
import Link from '@components/Link.astro'
|
import Link from '@components/Link.astro'
|
||||||
import { badgeVariants } from '@components/ui/badge'
|
import { badgeVariants } from '@components/ui/badge'
|
||||||
import { Hash, HomeIcon } from 'lucide-react'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import {
|
import { getCollection } from 'astro:content'
|
||||||
Breadcrumb,
|
import { Hash } from 'lucide-react'
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from '@/components/ui/breadcrumb'
|
|
||||||
|
|
||||||
const blog = (await getCollection('blog')).filter((post) => !post.data.draft)
|
const blog = (await getCollection('blog')).filter((post) => !post.data.draft)
|
||||||
|
|
||||||
|
@ -22,21 +15,8 @@ const tags = blog
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Tags" description="A list of all tags used in blog posts">
|
<Layout title="Tags" description="A list of all tags used in blog posts">
|
||||||
<Container>
|
<Container class="flex flex-col gap-y-6">
|
||||||
<div class="space-y-6">
|
<Breadcrumbs items={[{ label: 'Tags' }]} />
|
||||||
<Breadcrumb>
|
|
||||||
<BreadcrumbList>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbLink href="/"
|
|
||||||
><HomeIcon className="size-4" /></BreadcrumbLink
|
|
||||||
>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>Tags</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<h1 class="text-3xl font-semibold">Tags</h1>
|
<h1 class="text-3xl font-semibold">Tags</h1>
|
||||||
|
@ -54,6 +34,5 @@ const tags = blog
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -70,6 +70,7 @@
|
||||||
html {
|
html {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
scrollbar-gutter: stable both-edges;
|
scrollbar-gutter: stable both-edges;
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
|
||||||
&.dark {
|
&.dark {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
|