feat: polishing

This commit is contained in:
enscribe 2024-09-12 13:03:09 -07:00
parent 77bf1bbdf4
commit 0b430e5d43
No known key found for this signature in database
GPG key ID: 9BBD5C4114E25322
21 changed files with 235 additions and 144 deletions

View file

@ -35,8 +35,8 @@ export default defineConfig({
rehypePrettyCode,
{
theme: {
light: 'vitesse-light',
dark: 'vitesse-dark',
light: 'github-light-high-contrast',
dark: 'github-dark-high-contrast',
},
transformers: [
transformerNotationDiff(),

View file

@ -16,7 +16,7 @@ const { entry } = Astro.props as {
const formattedDate = formatDate(entry.data.date)
const readTime = readingTime(entry.body)
const authors = await parseAuthors(entry.data.author ?? [])
const authors = await parseAuthors(entry.data.authors ?? [])
---
<div

View file

@ -7,6 +7,7 @@ type Props = {
class?: string
underline?: boolean
'data-heading'?: string
[key: string]: any
}
const {

View file

@ -19,7 +19,7 @@ const { prevPost, nextPost } = Astro.props
>
<div class="mr-2 flex-shrink-0">
<ArrowLeft
className="h-4 w-4 transition-transform group-hover:-translate-x-1"
className="size-4 transition-transform group-hover:-translate-x-1"
/>
</div>
<div class="flex flex-col items-start overflow-hidden">
@ -46,7 +46,7 @@ const { prevPost, nextPost } = Astro.props
</div>
<div class="ml-2 flex-shrink-0">
<ArrowRight
className="h-4 w-4 transition-transform group-hover:translate-x-1"
className="size-4 transition-transform group-hover:translate-x-1"
/>
</div>
</Link>

View file

@ -1,37 +1,59 @@
---
import { Twitter, Github, Linkedin, Mail, Rss } from 'lucide-react'
import { SITE } from '@consts'
import { buttonVariants } from '@/components/ui/button'
import Link from '@components/Link.astro'
---
<ul class="not-prose flex flex-wrap gap-2" role="list">
<li>
<a href="#" class="inline-block" aria-label="Twitter" title="Twitter">
<Twitter />
</a>
<Link
href="#"
aria-label="Twitter"
title="Twitter"
class={buttonVariants({ variant: 'outline', size: 'icon' })}
>
<Twitter className="size-4" />
</Link>
</li>
<li>
<a href="#" class="inline-block" aria-label="GitHub" title="GitHub">
<Github />
</a>
<Link
href="#"
aria-label="GitHub"
title="GitHub"
class={buttonVariants({ variant: 'outline', size: 'icon' })}
>
<Github className="size-4" />
</Link>
</li>
<li>
<a href="#" class="inline-block" aria-label="LinkedIn" title="LinkedIn">
<Linkedin />
</a>
<Link
href="#"
aria-label="LinkedIn"
title="LinkedIn"
class={buttonVariants({ variant: 'outline', size: 'icon' })}
>
<Linkedin className="size-4" />
</Link>
</li>
<li>
<a href="#" class="inline-block" aria-label="Email" title="Email">
<Mail />
</a>
<Link
href="#"
aria-label="Email"
title="Email"
class={buttonVariants({ variant: 'outline', size: 'icon' })}
>
<Mail className="size-4" />
</Link>
</li>
<li>
<a
<Link
href={`${SITE.SITEURL}/rss.xml`}
class="inline-block"
aria-label="RSS feed"
title="RSS feed"
class={buttonVariants({ variant: 'outline', size: 'icon' })}
>
<Rss />
</a>
<Rss className="size-4" />
</Link>
</li>
</ul>

View file

@ -31,7 +31,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props}>
<Hash className="mr-0.5 h-3 w-3" />
<Hash className="size-3 -translate-x-0.5" />
{props.children}
</div>
)

View file

@ -98,7 +98,7 @@ const BreadcrumbEllipsis = ({
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<DotsHorizontalIcon className="h-4 w-4" />
<DotsHorizontalIcon className="size-4" />
<span className="sr-only">More</span>
</span>
)

View file

@ -36,7 +36,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
@ -110,7 +110,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
@ -133,7 +133,7 @@ const DropdownMenuRadioItem = React.forwardRef<
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
<DotFilledIcon className="size-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}

View file

@ -1,4 +1,4 @@
import { Moon, Sun } from 'lucide-react'
import { Laptop, Moon, Sun } from 'lucide-react'
import * as React from 'react'
import { Button } from '@/components/ui/button'
@ -30,21 +30,29 @@ export function ModeToggle() {
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<Button
variant="outline"
size="icon"
className="group"
title="Toggle theme"
>
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setThemeState('theme-light')}>
Light
<Sun className="mr-2 size-4" />
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setThemeState('dark')}>
Dark
<Moon className="mr-2 size-4" />
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setThemeState('system')}>
System
<Laptop className="mr-2 size-4" />
<span>System</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View file

@ -4,7 +4,7 @@ description: 'This a dummy post written in the year 2023.'
date: '2023-01-01'
tags: ['dummy', 'placeholder']
image: '/1200x630.png'
author: ['enscribe']
authors: ['enscribe']
---
This is a dummy post written in the year 2023.

View file

@ -1,10 +1,10 @@
---
title: '2024 Post'
description: 'This a dummy post written in the year 2024.'
description: 'This a dummy post written in the year 2024 (with multiple authors).'
date: '2024-01-01'
tags: ['dummy', 'placeholder']
image: '/1200x630.png'
author: ['enscribe']
authors: ['enscribe', 'jktrn']
---
This is a dummy post written in the year 2024.

View file

@ -4,7 +4,7 @@ description: 'There should not be a single reason why you would need a command p
date: '2024-07-25'
tags: ['webdev', 'opinion']
image: '/1200x630.png'
author: ['enscribe', 'jktrn']
authors: ['enscribe']
---
## Introduction
@ -15,7 +15,7 @@ I have a lot of opinions about what makes a great blogging template. As a cumula
astro-erudite is written in Astro, a framework hyperoptimized for static content such as blogs. Aesthetically, it is also designed to be as boring as possible while still maintaining maximum functionality, as to allow for the freedom of the developer (or the designer they hire) to make their blog uniquely their own. Within the codebase of this template I've included many nuances that, in my opinion (and there will be many, many opinions here), make the developer experience significantly more pleasant. I've also _excluded_ many features that, frankly, you don't need.
### Welcoming some DX features
## Welcoming some DX features
This is a non-exhaustive list of features I believe are essential for a frictionless developer experience:
@ -48,7 +48,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:
```tsx
```tsx title="src/lib/utils.ts" caption="A utility function for class name concatenation" showLineNumbers
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
@ -59,7 +59,7 @@ This is a non-exhaustive list of features I believe are essential for a friction
This needs to be in every single template. This is an example of it being used in my `<Link>{:tsx}` component:
```astro showLineNumbers title="src/components/Link.astro" caption="A custom Link component with tailwind-merge and clsx" {10-14}
```astro showLineNumbers title="src/components/Link.astro" caption="A custom Link component with tailwind-merge and clsx" {17-21}
---
import { cn } from '@lib/utils'
@ -95,7 +95,7 @@ This is a non-exhaustive list of features I believe are essential for a friction
Awesome!
### Welcoming some UX features
## Welcoming some UX features
Within the blog itself (as in the layout, appearance, and navigation) are features that I believe are essential for a great user experience:
@ -103,9 +103,11 @@ Within the blog itself (as in the layout, appearance, and navigation) are featur
- Theme selectors should be self-explanatory. I've added one on the top right of the header, which is also `sticky` and not `absolute` such that it doesn't ignore the document flow (and thus you won't have to add `mt-20` to the top of every single page).
- The table of contents of a post shouldn't be reduced to a `<details closed>{:html}` at the start of a blog post on desktop. You'd need to go to the top of the page to navigate through items. I've added a sticky `TableOfContents` component which always hangs out around the unused left side margin of a blog post. I'll still use a collapsible `<details>{:html}` element for the table of contents on mobile though since obviously a table of contents on the side is unfeasible for small screens.
- Every page, except the homepage, will have a `<Breadcrumb>{:tsx}` component which shows you your current location in the site hierarchy. I don't see these often in blog templates even though they are so amazing for both discoverability (SEO and crawling) and user experience (the user always knows how "deep" they are in the site).
- You can specify a post author via frontmatter. If this post author's slug is found within the `Authors` collection, then it will render particular info from that author's frontmatter file, `[author-name].md` (e.g. avatar, link to profile).
- You can specify multiple post authors via frontmatter. If this post author's slug is found within the `Authors` collection, then it will render particular info from that author's frontmatter file, `[author-name].md` (e.g. avatar, link to profile). For example, the previous post (2024 Post) has two authors: "enscribe" and "jktrn", where "enscribe" is the only author with a custom avatar since "jktrn" is unregistered.
- Each author will have their own page, which lists all of their posts. If you're the only author throughout the entire blog then you can simply disregard all aspects regarding both inserting authors and the `Authors` collection.
- Each tag will also have their own page, which lists all of the posts under that tag!
### Foregoing some slop
## Foregoing some slop
- Goodbye, [ESLint](https://eslint.org/)! There have been so many occasions where I've had to deal with blogging templates with in-built pre-commit hooks which enforce contrived and arbitrary linting rules that, frankly, I couldn't be bothered with. Obviously, linting is awesome for ensuring consistency and best practice, but that's with shared and large codebases. You're dealing with, at most, your mediocre MDX blog posts and some interior fetching. It's just not worth the hypertension.

View file

@ -9,7 +9,7 @@ const blog = defineCollection({
draft: z.boolean().optional(),
image: z.string().optional(),
tags: z.array(z.string()).optional(),
author: z.array(z.string()).optional(),
authors: z.array(z.string()).optional(),
}),
})

View file

@ -22,7 +22,7 @@ import { cn } from '@lib/utils'
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/"
><HomeIcon className="h-4 w-4" /></BreadcrumbLink
><HomeIcon className="size-4" /></BreadcrumbLink
>
</BreadcrumbItem>
<BreadcrumbSeparator />
@ -37,7 +37,7 @@ import { cn } from '@lib/utils'
>
<div class="max-w-md">
<h1 class="mb-4 text-3xl font-bold">404: Page not found</h1>
<p class="prose dark:prose-invert">
<p class="prose prose-neutral dark:prose-invert">
Oops! The page you're looking for doesn't exist.
</p>
</div>

View file

@ -20,7 +20,7 @@ import { HomeIcon } from 'lucide-react'
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/"
><HomeIcon className="h-4 w-4" /></BreadcrumbLink
><HomeIcon className="size-4" /></BreadcrumbLink
>
</BreadcrumbItem>
<BreadcrumbSeparator />
@ -33,7 +33,7 @@ import { HomeIcon } from 'lucide-react'
<section>
<div class="min-w-full">
<h1 class="mb-4 text-3xl font-bold">Some more about us</h1>
<p class="prose dark:prose-invert">
<p class="prose prose-neutral dark:prose-invert">
{SITE.TITLE} is an opinionated, no-frills static blogging template built
with Astro.
</p>

View file

@ -32,12 +32,7 @@ const { author } = Astro.props
const allPosts = await getCollection('blog')
const authorPosts = allPosts
.filter((post) => {
if (typeof post.data.author === 'string') {
return post.data.author === author.data.name && !post.data.draft
} else if (post.data.author && 'slug' in post.data.author) {
return post.data.author.slug === author.slug && !post.data.draft
}
return false
return post.data.authors && post.data.authors.includes(author.slug)
})
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
---
@ -51,7 +46,7 @@ const authorPosts = allPosts
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/"
><HomeIcon className="h-4 w-4" /></BreadcrumbLink
><HomeIcon className="size-4" /></BreadcrumbLink
>
</BreadcrumbItem>
<BreadcrumbSeparator />

View file

@ -22,7 +22,7 @@ const authors = await getCollection('authors')
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/"
><HomeIcon className="h-4 w-4" /></BreadcrumbLink
><HomeIcon className="size-4" /></BreadcrumbLink
>
</BreadcrumbItem>
<BreadcrumbSeparator />

View file

@ -1,5 +1,5 @@
---
import { type CollectionEntry, getCollection, getEntry } from 'astro:content'
import { type CollectionEntry, getCollection } from 'astro:content'
import Layout from '@layouts/Layout.astro'
import Container from '@components/Container.astro'
import { formatDate, readingTime } from '@lib/utils'
@ -59,7 +59,7 @@ const prevPost = getPrevPost(currentPostSlug)
const post = Astro.props
const { Content, headings } = await post.render()
const authors = await parseAuthors(post.data.author ?? [])
const authors = await parseAuthors(post.data.authors ?? [])
---
<Layout title={post.data.title} description={post.data.description}>
@ -68,7 +68,7 @@ const authors = await parseAuthors(post.data.author ?? [])
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/"
><HomeIcon className="h-4 w-4" /></BreadcrumbLink
><HomeIcon className="size-4" /></BreadcrumbLink
>
</BreadcrumbItem>
<BreadcrumbSeparator />
@ -147,7 +147,7 @@ const authors = await parseAuthors(post.data.author ?? [])
href={`/tags/${tag}`}
class={badgeVariants({ variant: 'secondary' })}
>
<Hash className="mr-0.5 h-3 w-3" />
<Hash className="-translate-x-0.5 size-3" />
{tag}
</a>
))
@ -162,7 +162,7 @@ const authors = await parseAuthors(post.data.author ?? [])
{headings.length > 0 && <TableOfContents headings={headings} />}
<article class="prose max-w-none dark:prose-invert">
<article class="prose prose-neutral max-w-none dark:prose-invert">
<Content />
</article>
@ -185,13 +185,21 @@ const authors = await parseAuthors(post.data.author ?? [])
<script>
document.addEventListener('astro:page-load', () => {
const scrollToTopButton = document.getElementById('scroll-to-top')
if (scrollToTopButton) {
const footer = document.querySelector('footer')
if (scrollToTopButton && footer) {
scrollToTopButton.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
})
window.addEventListener('scroll', () => {
scrollToTopButton.classList.toggle('hidden', window.scrollY <= 300)
const footerRect = footer.getBoundingClientRect()
const isFooterVisible = footerRect.top <= window.innerHeight
scrollToTopButton.classList.toggle(
'hidden',
window.scrollY <= 300 || isFooterVisible,
)
})
}
})

View file

@ -39,7 +39,7 @@ const years = Object.keys(posts).sort((a, b) => parseInt(b) - parseInt(a))
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/"
><HomeIcon className="h-4 w-4" /></BreadcrumbLink
><HomeIcon className="size-4" /></BreadcrumbLink
>
</BreadcrumbItem>
<BreadcrumbSeparator />

View file

@ -2,7 +2,17 @@
import { getCollection } from 'astro:content'
import Layout from '@layouts/Layout.astro'
import Container from '@components/Container.astro'
import { Badge } from '@/components/ui/badge'
import Link from '@components/Link.astro'
import { badgeVariants } from '@components/ui/badge'
import { Hash, HomeIcon } from 'lucide-react'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
const blog = (await getCollection('blog')).filter((post) => !post.data.draft)
@ -11,20 +21,38 @@ const tags = blog
.filter((tag, index, self) => self.indexOf(tag) === index)
---
<Layout title="Tags" description="Tags">
<Layout title="Tags" description="A list of all tags used in blog posts">
<Container>
<div class="space-y-10">
<h1 class="text-3xl font-semibold">Tags</h1>
<div class="flex flex-wrap gap-2">
{
tags.map((tag) => (
<a href={`/tags/${tag}`}>
<Badge variant="secondary" className="hover:bg-secondary/80">
#{tag}
</Badge>
</a>
))
}
<div class="space-y-6">
<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">
<h1 class="text-3xl font-semibold">Tags</h1>
<div class="flex flex-wrap gap-2">
{
tags.map((tag) => (
<Link
href={`/tags/${tag}`}
class={badgeVariants({ variant: 'secondary' })}
>
<Hash className="-translate-x-0.5 size-3" />
{tag}
</Link>
))
}
</div>
</div>
</div>
</Container>

View file

@ -64,84 +64,111 @@
*,
*::before,
*::after {
@apply !border-border;
@apply border-border;
}
html {
color-scheme: light;
scrollbar-gutter: stable both-edges;
}
html.dark {
color-scheme: dark;
}
&.dark {
color-scheme: dark;
}
::-webkit-scrollbar-corner {
@apply bg-transparent;
::-webkit-scrollbar-corner {
@apply bg-transparent;
}
}
}
@layer components {
code[data-theme*=' '] span {
color: var(--shiki-light);
}
.dark code[data-theme*=' '] span {
color: var(--shiki-dark);
}
pre {
@apply max-h-[600px] overflow-auto rounded-xl border bg-secondary/20 py-4 text-sm leading-loose;
}
pre > code {
counter-reset: line;
@apply whitespace-pre-wrap;
}
code[data-line-numbers] {
counter-reset: line;
}
code[data-line-numbers] > [data-line]::before {
counter-increment: line;
content: counter(line);
@apply mr-4 inline-block w-4 text-right text-muted-foreground;
}
pre > code > span[data-line] {
@apply px-4;
}
[data-highlighted-line] {
@apply !bg-foreground/10;
}
[data-highlighted-chars] > span {
@apply rounded-md border !border-muted-foreground/40 !bg-muted p-1 font-semibold;
}
[data-rehype-pretty-code-title] {
@apply rounded-t-xl border-x border-t px-4 py-2 text-sm font-medium !text-foreground;
}
[data-rehype-pretty-code-title] + pre {
@apply mt-0 rounded-t-none;
}
.diff.add {
@apply bg-additive/15;
}
.diff.remove {
@apply bg-destructive/15;
}
article :not(pre) > code {
@apply relative rounded bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium;
}
article {
@apply prose-headings:scroll-mt-20;
/* Removes background from <mark> elements */
mark {
@apply bg-transparent;
}
/* Blanket syntax highlighting */
code[data-theme*=' '] {
span {
color: var(--shiki-light);
}
.dark & span {
color: var(--shiki-dark);
}
}
/* Inline code */
:not(pre) > code {
@apply relative rounded bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium;
}
/* Code blocks */
figure[data-rehype-pretty-code-figure] {
@apply relative;
/* Code block titles */
[data-rehype-pretty-code-title] {
@apply rounded-t-xl border-x border-t px-4 py-2 text-sm font-medium !text-foreground;
/* Remove top margin from code block if a title is present */
& + pre {
@apply mt-0 rounded-t-none;
}
}
/* Code block styles */
pre {
@apply static max-h-[600px] overflow-auto rounded-xl border bg-secondary/20 py-4 text-sm leading-loose;
/* Code block content */
> code {
@apply whitespace-pre-wrap;
counter-reset: line;
/* For code blocks with line numbers */
&[data-line-numbers] {
> [data-line]::before {
counter-increment: line;
content: counter(line);
@apply mr-4 inline-block w-4 text-right text-muted-foreground;
}
}
/* For each line in the code block */
> [data-line] {
@apply px-4;
}
/* Highlighted lines */
[data-highlighted-line] {
@apply bg-foreground/10;
}
/* Highlighted characters */
[data-highlighted-chars] > span {
@apply bg-muted-foreground/40 py-[7px];
}
/* Diff lines */
.diff {
&.add {
@apply bg-additive/15;
}
&.remove {
@apply bg-destructive/15;
}
}
/* Copy button */
> button:has(> span) {
@apply right-0.5 top-[3px] m-0 size-8 rounded-md bg-transparent p-1 backdrop-blur-none;
}
}
}
}
}
}