feat: revamped author card

This commit is contained in:
enscribe 2024-09-12 18:08:19 -07:00
parent c410c499e1
commit 2211c4bbf3
No known key found for this signature in database
GPG key ID: 9BBD5C4114E25322
46 changed files with 566 additions and 426 deletions

173
package-lock.json generated
View file

@ -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",

View file

@ -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": [
{ {

View file

Before

Width:  |  Height:  |  Size: 856 B

After

Width:  |  Height:  |  Size: 856 B

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

Before

Width:  |  Height:  |  Size: 838 B

After

Width:  |  Height:  |  Size: 838 B

View file

@ -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>

View file

Before

Width:  |  Height:  |  Size: 495 B

After

Width:  |  Height:  |  Size: 495 B

View file

Before

Width:  |  Height:  |  Size: 598 B

After

Width:  |  Height:  |  Size: 598 B

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

View file

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 877 B

View file

@ -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"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -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>

View file

@ -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 = {

View 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>

View file

@ -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" />

View file

@ -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"

View file

@ -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(

View file

@ -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">

View file

@ -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"
> >

View file

@ -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
--- ---

View file

@ -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>

View file

@ -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',
{ {

View file

@ -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'> & {

View file

@ -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',

View file

@ -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,

View file

@ -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,
)} )}

View file

@ -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<

View file

@ -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>

View file

@ -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' },
]

View file

@ -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'

View file

@ -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&mdash;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[]) {

View file

@ -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(),

View file

@ -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">

View file

@ -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,
} }
} }

View file

@ -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"

View file

@ -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">

View file

@ -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>

View file

@ -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) => (

View file

@ -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>

View file

@ -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">
{ {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;