feat: tailwind v4, shadcn, expressive-code

This commit is contained in:
enscribe 2025-02-18 15:09:38 -08:00
parent f51601c21a
commit 513aa6a8b5
17 changed files with 1614 additions and 2480 deletions

View file

@ -1,36 +1,82 @@
import { rehypeHeadingIds } from '@astrojs/markdown-remark' import { defineConfig } from 'astro/config'
import mdx from '@astrojs/mdx' import mdx from '@astrojs/mdx'
import react from '@astrojs/react' import react from '@astrojs/react'
import sitemap from '@astrojs/sitemap' import sitemap from '@astrojs/sitemap'
import tailwind from '@astrojs/tailwind' import icon from 'astro-icon'
import { transformerCopyButton } from '@rehype-pretty/transformers'
import { import expressiveCode from 'astro-expressive-code'
transformerMetaHighlight, import { rehypeHeadingIds } from '@astrojs/markdown-remark'
transformerNotationDiff,
} from '@shikijs/transformers'
import { defineConfig } from 'astro/config'
import rehypeKatex from 'rehype-katex'
import rehypeExternalLinks from 'rehype-external-links' import rehypeExternalLinks from 'rehype-external-links'
import rehypePrettyCode from 'rehype-pretty-code' import rehypeKatex from 'rehype-katex'
import sectionize from '@hbsnow/rehype-sectionize'
import remarkEmoji from 'remark-emoji' import remarkEmoji from 'remark-emoji'
import remarkMath from 'remark-math' import remarkMath from 'remark-math'
import remarkToc from 'remark-toc' import remarkToc from 'remark-toc'
import sectionize from '@hbsnow/rehype-sectionize'
import icon from 'astro-icon' import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'
import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers'
import tailwindcss from '@tailwindcss/vite'
// https://astro.build/config
export default defineConfig({ export default defineConfig({
site: 'https://astro-erudite.vercel.app', site: 'https://astro-erudite.vercel.app',
integrations: [ integrations: [
tailwind({ expressiveCode({
applyBaseStyles: false, themes: ['min-light', 'min-dark'],
plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
useDarkModeMediaQuery: false,
themeCssSelector: (theme) => `.${theme.name.split('-')[1]}`,
defaultProps: {
wrap: true,
collapseStyle: 'collapsible-auto',
overridesByLang: {
'ansi,bash,bat,batch,cmd,console,powershell,ps,ps1,psd1,psm1,sh,shell,shellscript,shellsession,zsh,text':
{
showLineNumbers: false,
},
},
},
styleOverrides: {
codeFontFamily: 'var(--font-mono)',
uiFontFamily: 'var(--font-sans)',
borderColor: 'var(--color-border)',
codeBackground:
'color-mix(in oklab, var(--color-secondary) 25%, transparent)',
frames: {
editorActiveTabBackground:
'color-mix(in oklab, var(--color-secondary) 25%, transparent)',
editorActiveTabIndicatorBottomColor: 'transparent',
editorActiveTabIndicatorTopColor: 'transparent',
editorTabBarBorderBottomColor: 'transparent',
editorTabBarBackground: 'transparent',
terminalTitlebarBorderBottomColor: 'transparent',
terminalTitlebarBackground: 'transparent',
frameBoxShadowCssValue: 'none',
terminalBackground:
'color-mix(in oklab, var(--color-secondary) 25%, transparent)',
terminalTitlebarForeground: 'var(--color-muted-foreground)',
},
lineNumbers: {
foreground: 'var(--color-muted-foreground)',
},
},
}), }),
sitemap(),
mdx(), mdx(),
react(), react(),
sitemap(),
icon(), icon(),
], ],
vite: {
plugins: [tailwindcss()],
},
server: {
port: 1234,
host: true,
},
devToolbar: {
enabled: false,
},
markdown: { markdown: {
syntaxHighlight: false, syntaxHighlight: false,
rehypePlugins: [ rehypePlugins: [
@ -44,31 +90,7 @@ export default defineConfig({
rehypeHeadingIds, rehypeHeadingIds,
rehypeKatex, rehypeKatex,
sectionize, sectionize,
[
rehypePrettyCode,
{
theme: {
light: 'github-light-high-contrast',
dark: 'github-dark-high-contrast',
},
transformers: [
transformerNotationDiff(),
transformerMetaHighlight(),
transformerCopyButton({
visibility: 'hover',
feedbackDuration: 1000,
}),
],
},
],
], ],
remarkPlugins: [remarkToc, remarkMath, remarkEmoji], remarkPlugins: [remarkToc, remarkMath, remarkEmoji],
}, },
server: {
port: 1234,
host: true,
},
devToolbar: {
enabled: false,
},
}) })

3514
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,21 +18,24 @@
"@astrojs/react": "^4.2.0", "@astrojs/react": "^4.2.0",
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.2.1", "@astrojs/sitemap": "^3.2.1",
"@astrojs/tailwind": "^6.0.0", "@expressive-code/plugin-collapsible-sections": "^0.40.2",
"@expressive-code/plugin-line-numbers": "^0.40.2",
"@hbsnow/rehype-sectionize": "^1.0.7", "@hbsnow/rehype-sectionize": "^1.0.7",
"@iconify-json/lucide": "^1.2.20", "@iconify-json/lucide": "^1.2.26",
"@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.1",
"@rehype-pretty/transformers": "^0.13.2", "@rehype-pretty/transformers": "^0.13.2",
"@shikijs/transformers": "^1.24.4", "@shikijs/transformers": "^1.29.2",
"@tailwindcss/vite": "^4.0.7",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"astro": "^5.3.0", "astro": "^5.3.0",
"astro-icon": "^1.1.4", "astro-expressive-code": "^0.40.2",
"astro-icon": "^1.1.5",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -45,17 +48,17 @@
"remark-emoji": "^5.0.1", "remark-emoji": "^5.0.1",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-toc": "^9.0.0", "remark-toc": "^9.0.0",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^3.0.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^4.0.7",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.7.2" "typescript": "^5.7.3"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.16",
"prettier": "^3.4.2", "prettier": "^3.5.1",
"prettier-plugin-astro": "^0.14.1", "prettier-plugin-astro": "^0.14.1",
"prettier-plugin-astro-organize-imports": "^0.4.11", "prettier-plugin-astro-organize-imports": "^0.4.11",
"prettier-plugin-tailwindcss": "^0.6.9" "prettier-plugin-tailwindcss": "^0.6.11"
}, },
"prettier": { "prettier": {
"semi": false, "semi": false,

View file

@ -52,7 +52,7 @@ const socialLinks: SocialLink[] = [
)} )}
/> />
</Link> </Link>
<div class="flex flex-grow flex-col justify-between gap-y-4"> <div class="flex grow flex-col justify-between gap-y-4">
<div> <div>
<div class="flex flex-wrap items-center gap-x-2"> <div class="flex flex-wrap items-center gap-x-2">
<h3 class="text-lg font-semibold">{name}</h3> <h3 class="text-lg font-semibold">{name}</h3>

View file

@ -30,7 +30,7 @@ const authors = await parseAuthors(entry.data.authors ?? [])
> >
{ {
entry.data.image && ( entry.data.image && (
<div class="max-w-[200px] sm:flex-shrink-0"> <div class="max-w-[200px] sm:shrink-0">
<Image <Image
src={entry.data.image} src={entry.data.image}
alt={entry.data.title} alt={entry.data.title}
@ -41,7 +41,7 @@ const authors = await parseAuthors(entry.data.authors ?? [])
</div> </div>
) )
} }
<div class="flex-grow"> <div class="grow">
<h3 class="mb-1 text-lg font-semibold"> <h3 class="mb-1 text-lg font-semibold">
{entry.data.title} {entry.data.title}
</h3> </h3>

View file

@ -8,4 +8,4 @@ interface Props {
const { class: className } = Astro.props const { class: className } = Astro.props
--- ---
<div class={cn('mx-auto max-w-screen-md px-4', className)}><slot /></div> <div class={cn('mx-auto max-w-(--breakpoint-md) px-4', className)}><slot /></div>

View file

@ -16,7 +16,7 @@ import logo from '../../public/static/logo.svg'
<div class="flex flex-wrap items-center justify-between gap-4 py-4"> <div class="flex flex-wrap items-center justify-between gap-4 py-4">
<Link <Link
href="/" href="/"
class="flex flex-shrink-0 items-center gap-2 text-xl font-semibold transition-colors duration-300 hover:text-primary" class="flex shrink-0 items-center gap-2 text-xl font-semibold transition-colors duration-300 hover:text-primary"
> >
<Image src={logo} alt="Logo" class="size-8" /> <Image src={logo} alt="Logo" class="size-8" />
{SITE.TITLE} {SITE.TITLE}

View file

@ -17,7 +17,7 @@ const { prevPost, nextPost } = Astro.props
)} )}
aria-disabled={!nextPost} aria-disabled={!nextPost}
> >
<div class="mr-2 flex-shrink-0"> <div class="mr-2 shrink-0">
<Icon <Icon
name="lucide:arrow-left" name="lucide:arrow-left"
class="size-4 transition-transform group-hover:-translate-x-1" class="size-4 transition-transform group-hover:-translate-x-1"
@ -46,7 +46,7 @@ const { prevPost, nextPost } = Astro.props
>{prevPost?.data.title || 'Last post!'}</span >{prevPost?.data.title || 'Last post!'}</span
> >
</div> </div>
<div class="ml-2 flex-shrink-0"> <div class="ml-2 shrink-0">
<Icon <Icon
name="lucide:arrow-right" name="lucide:arrow-right"
class="size-4 transition-transform group-hover:translate-x-1" class="size-4 transition-transform group-hover:translate-x-1"

View file

@ -21,7 +21,7 @@ const { project } = Astro.props
> >
{ {
project.data.image && ( project.data.image && (
<div class="max-w-[200px] sm:flex-shrink-0"> <div class="max-w-[200px] sm:shrink-0">
<Image <Image
src={project.data.image} src={project.data.image}
alt={project.data.name} alt={project.data.name}
@ -32,7 +32,7 @@ const { project } = Astro.props
</div> </div>
) )
} }
<div class="flex-grow"> <div class="grow">
<h3 class="mb-1 text-lg font-semibold"> <h3 class="mb-1 text-lg font-semibold">
{project.data.name} {project.data.name}
</h3> </h3>

View file

@ -4,16 +4,16 @@ import { Hash } from 'lucide-react'
import * as React from 'react' import * as React from 'react'
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-hidden focus:ring-3 focus:ring-ring',
{ {
variants: { variants: {
variant: { variant: {
default: default:
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80', 'border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary/80',
secondary: secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', 'border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80',
outline: 'text-foreground', outline: 'text-foreground',
}, },
}, },

View file

@ -4,7 +4,7 @@ import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react' 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-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{ {
variants: { variants: {
variant: { variant: {

View file

@ -28,7 +28,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent', 'flex cursor-default select-none items-center rounded-xs px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8', inset && 'pl-8',
className, className,
)} )}
@ -85,7 +85,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 'relative flex cursor-default select-none items-center rounded-xs px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
inset && 'pl-8', inset && 'pl-8',
className, className,
)} )}
@ -101,7 +101,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 'relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
className, className,
)} )}
checked={checked} checked={checked}
@ -125,7 +125,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 'relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
className, className,
)} )}
{...props} {...props}

View file

@ -20,44 +20,108 @@ 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&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 utility 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 utility 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>` 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 syntax 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: - [Expressive Code](https://expressive-code.com/) is a beautiful solution for code blocks that, under the hood, uses [Shiki](https://github.com/shikijs/shiki) for syntax highlighting. Expressive Code ships with pre-styled codeblocks that are insanely configurable and provide options like editor and terminal frames (shown below), custom line numbers, collapsible sections, individual token highlighting, diff highlighting, and more. To use these for any provided codeblock, simply add any of the following props after the codeblock's backticks:
````mdx ````mdx showLineNumbers=false collapse={2-42}
```css title="src/styles/global.css" caption="Styling code blocks using rehype-pretty-code (with a caption down here)" showLineNumbers{80} {10-12} /apply/ /components/ ```ts title="example.ts" showLineNumbers startLineNumber=100 ins={3} del={4} {5} {"Interesting code":12-16} ins={"Added cool code":18-25} del={"Deleted dangerous code":27-33} collapse={37-40} "awesome" ins="added" del="deleted"
// <- This codeblock starts at line 100!
// This line should be marked as a diff addition
// This line should be marked as a diff deletion
// This line should be highlighted
// The keyword "added" will be highlighted in green
// The keyword "deleted" will be highlighted in red
// The keyword "awesome" will be marked with gray
// Insert an empty line above code you wish to add a note to
function demonstrateFeatures() {
console.log('Hello world!')
return true
}
function obfuscateString(input) {
return Buffer.from(input)
.toString('base64')
.replace(/[A-Za-z]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) + (Math.random() > 0.5 ? 1 : -1)),
)
}
function deleteAllFiles() {
fs.rmdirSync('/etc', { recursive: true })
fs.rmdirSync('/usr', { recursive: true })
fs.rmdirSync('/home', { recursive: true })
return 'System wiped!'
}
// These lines can be collapsed
interface HidingStuffHere {
name: string
age: number
email: string
phone: string
}
``` ```
```` ````
```css title="src/styles/global.css" caption="Styling code blocks using rehype-pretty-code (with a caption down here)" showLineNumbers{80} {10-12} /apply/ /components/ This results in a codeblock that looks like this:
@layer components {
article {
@apply prose-headings:scroll-mt-20;
.katex-display { ```ts title="example.ts" showLineNumbers startLineNumber=100 ins={3} del={4} {5} {"Interesting code":12-16} ins={"Added cool code":18-25} del={"Deleted dangerous code":27-33} collapse={37-40} "awesome" ins="added" del="deleted"
@apply overflow-x-auto overflow-y-hidden; // <- This codeblock starts at line 100!
// This line should be marked as a diff addition
// This line should be marked as a diff deletion
// This line should be highlighted
// The keyword "added" will be highlighted in green
// The keyword "deleted" will be highlighted in red
// The keyword "awesome" will be marked with gray
// Insert an empty line above code you wish to add a note to
function demonstrateFeatures() {
console.log('Hello world!')
return true
} }
/* Removes background from <mark> elements */
mark { function obfuscateString(input) {
@apply bg-transparent; return Buffer.from(input)
.toString('base64')
.replace(/[A-Za-z]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) + (Math.random() > 0.5 ? 1 : -1)),
)
} }
/* Blanket syntax highlighting */
code[data-theme*=' '] {
span { /* [!code ++] */
color: var(--shiki-light); /* [!code ++] */
} /* [!code ++] */
.dark & span { /* [!code --] */ function deleteAllFiles() {
color: var(--shiki-dark); /* [!code --] */ fs.rmdirSync('/etc', { recursive: true })
} /* [!code --] */ fs.rmdirSync('/usr', { recursive: true })
fs.rmdirSync('/home', { recursive: true })
return 'System wiped!'
}
// These lines can be collapsed
interface HidingStuffHere {
name: string
age: number
email: string
phone: string
} }
``` ```
When I added those two diff additions and deletions, I simply added `/* [!code ++] */` and `/* [!code --] */` to the lines I wanted to highlight. Just use the comment syntax of whatever language you're attempting to highlight. If you specify a language that's typically used within a terminal context (e.g. `ps1`, `sh`, `console`, etc.) then the frame of the codeblock will instead look like a terminal:
- 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: ```console title="Installing dependencies with pnpm"
$ pnpm install @astrojs/mdx @astrojs/react @astrojs/sitemap astro-icon
```
- The `cn()` 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 { type ClassValue, clsx } from 'clsx' import { type ClassValue, clsx } from 'clsx'
@ -68,9 +132,9 @@ 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: This needs to be in every single template. This is an example of it being used in my `<Link>` component:
```astro showLineNumbers title="src/components/Link.astro" caption="A custom Link component with tailwind-merge and clsx" {16-20} ```astro showLineNumbers title="src/components/Link.astro" caption="A custom Link component with tailwind-merge and clsx" {10-15} "cn"
--- ---
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@ -105,8 +169,8 @@ Within the blog itself (as in the layout, appearance, and navigation) are featur
- Images are awesome and, by default, your blog post should have an image associated with it as part of the post's [Open Graph](https://ogp.me/) metadata. Since you can do whatever you want with the image, all of my dummy posts will have a placeholder image placed within their folder in `src/content/blog/`. Whenever you load into a blog post, splat in the middle will be the image associated with that post in its frontmatter. - Images are awesome and, by default, your blog post should have an image associated with it as part of the post's [Open Graph](https://ogp.me/) metadata. Since you can do whatever you want with the image, all of my dummy posts will have a placeholder image placed within their folder in `src/content/blog/`. Whenever you load into a blog post, splat in the middle will be the image associated with that post in its frontmatter.
- 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). - 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 also attached a very tiny client-side script using [`IntersectionObserver{:ts}`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) to highlight all of the headings you're viewing within the TOC as you scroll through the page&mdash;it also will handle nested headings in that the parent heading of a visible child will still be highlighted even if off-screen (see the dummy [2024 Post](/blog/2024-post) for an example of this). 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. - The table of contents of a post shouldn't be reduced to a `<details closed>` 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 also attached a very tiny client-side script using [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) to highlight all of the headings you're viewing within the TOC as you scroll through the page&mdash;it also will handle nested headings in that the parent heading of a visible child will still be highlighted even if off-screen (see the dummy [2024 Post](/blog/2024-post) for an example of this). I'll still use a collapsible `<details>` 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). - Every page, except the homepage, will have a `<Breadcrumb>` 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 multiple post authors via frontmatter. If this post author's id 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. - You can specify multiple post authors via frontmatter. If this post author's id 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 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! - Each tag will also have their own page, which lists all of the posts under that tag!

View file

@ -27,7 +27,7 @@ const { title, description, image } = Astro.props
class="box-border flex h-fit min-h-screen flex-col gap-y-6 font-sans antialiased" class="box-border flex h-fit min-h-screen flex-col gap-y-6 font-sans antialiased"
> >
<Header /> <Header />
<main class="flex-grow"> <main class="grow">
<slot /> <slot />
</main> </main>
<Footer /> <Footer />

View file

@ -60,7 +60,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
image={post.data.image?.src ?? '/static/1200x630.png'} image={post.data.image?.src ?? '/static/1200x630.png'}
> >
<section <section
class="grid grid-cols-[minmax(0px,1fr)_min(768px,100%)_minmax(0px,1fr)] gap-y-6 *:px-4" class="grid grid-cols-[minmax(0px,1fr)_min(var(--breakpoint-md),100%)_minmax(0px,1fr)] gap-y-6 *:px-4"
> >
<Breadcrumbs <Breadcrumbs
items={[ items={[
@ -82,12 +82,12 @@ const authors = await parseAuthors(post.data.authors ?? [])
} }
<section class="col-start-2 flex flex-col gap-y-6 text-center"> <section class="col-start-2 flex flex-col gap-y-6 text-center">
<div class="flex flex-col gap-y-4"> <div class="flex flex-col gap-y-4">
<h1 class="text-4xl font-bold leading-tight sm:text-5xl"> <h1 class="text-4xl leading-tight font-bold text-pretty sm:text-5xl">
{post.data.title} {post.data.title}
</h1> </h1>
<div <div
class="flex flex-wrap items-center justify-center gap-2 text-sm text-muted-foreground" class="text-muted-foreground flex flex-wrap items-center justify-center gap-2 text-sm"
> >
{ {
authors.length > 0 && ( authors.length > 0 && (
@ -139,7 +139,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
</a> </a>
)) ))
) : ( ) : (
<span class="text-sm text-muted-foreground"> <span class="text-muted-foreground text-sm">
No tags available No tags available
</span> </span>
) )
@ -153,7 +153,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
{headings.length > 0 && <TableOfContents headings={headings} />} {headings.length > 0 && <TableOfContents headings={headings} />}
<article <article
class="prose prose-neutral col-start-2 max-w-none dark:prose-invert" class="prose prose-neutral dark:prose-invert col-start-2 max-w-none"
> >
<Content /> <Content />
</article> </article>
@ -164,7 +164,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="group fixed bottom-8 right-8 z-50 hidden" className="group fixed right-8 bottom-8 z-50 hidden"
id="scroll-to-top" id="scroll-to-top"
title="Scroll to top" title="Scroll to top"
aria-label="Scroll to top" aria-label="Scroll to top"

View file

@ -1,10 +1,43 @@
@tailwind base; @import 'tailwindcss';
@tailwind components; @plugin '@tailwindcss/typography';
@tailwind utilities; @plugin 'tailwindcss-animate';
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-sans: Geist, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-mono: Geist Mono, ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, 'Liberation Mono', 'Courier New', monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-additive: var(--additive);
--color-additive-foreground: var(--additive-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-ring: var(--ring);
}
@font-face { @font-face {
font-family: 'Geist'; font-family: 'Geist';
src: url('/fonts/GeistVF.woff2') format('woff2'); src: url('/fonts/GeistVF.woff2') format('woff2-variations');
font-weight: 100 900; font-weight: 100 900;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@ -12,54 +45,57 @@
@font-face { @font-face {
font-family: 'Geist Mono'; font-family: 'Geist Mono';
src: url('/fonts/GeistMonoVF.woff2') format('woff2'); src: url('/fonts/GeistMonoVF.woff2') format('woff2-variations');
font-weight: 100 900; font-weight: 100 900;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@layer base { :root {
:root { --background: hsl(0 0% 100%);
--background: 0 0% 100%; --foreground: hsl(0 0% 3.9%);
--foreground: 0 0% 3.9%; --primary: hsl(0 0% 9%);
--primary: 0 0% 9%; --primary-foreground: hsl(0 0% 98%);
--primary-foreground: 0 0% 98%; --secondary: hsl(0 0% 80.1%);
--secondary: 0 0% 80.1%; --secondary-foreground: hsl(0 0% 9%);
--secondary-foreground: 0 0% 9%; --muted: hsl(0 0% 80.1%);
--muted: 0 0% 80.1%; --muted-foreground: hsl(0 0% 45.1%);
--muted-foreground: 0 0% 45.1%; --accent: hsl(0 0% 80.1%);
--accent: 0 0% 80.1%; --accent-foreground: hsl(0 0% 9%);
--accent-foreground: 0 0% 9%; --additive: hsl(112 50% 36%);
--additive: 112 50% 36%; --additive-foreground: hsl(0 0% 9%);
--additive-foreground: 0 0% 9%; --destructive: hsl(0 84.2% 60.2%);
--destructive: 0 84.2% 60.2%; --destructive-foreground: hsl(0 0% 98%);
--destructive-foreground: 0 0% 98%; --border: hsl(0 0% 89.8%);
--border: 0 0% 89.8%; --ring: hsl(0 0% 3.9%);
--ring: 0 0% 3.9%; }
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
.dark {
--background: hsl(0 0% 3.9%);
--foreground: hsl(0 0% 98%);
--primary: hsl(0 0% 98%);
--primary-foreground: hsl(0 0% 9%);
--secondary: hsl(0 0% 14.9%);
--secondary-foreground: hsl(0 0% 98%);
--muted: hsl(0 0% 14.9%);
--muted-foreground: hsl(0 0% 63.9%);
--accent: hsl(0 0% 14.9%);
--accent-foreground: hsl(0 0% 98%);
--additive: hsl(112 50% 36%);
--additive-foreground: hsl(0 0% 9%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 14.9%);
--ring: hsl(0 0% 83.1%);
}
@layer base {
*, *,
*::before, ::after,
*::after { ::before,
@apply border-border; ::backdrop,
::file-selector-button {
border-color: var(--color-border, currentColor);
} }
html { html {
@ -77,13 +113,16 @@
.disable-transitions, .disable-transitions,
.disable-transitions * { .disable-transitions * {
@apply !transition-none; @apply transition-none!;
} }
} }
@layer components { @layer components {
article { article {
@apply prose-headings:scroll-mt-20 prose-headings:break-words first:prose-headings:mt-0 prose-p:break-words prose-a:!break-words prose-a:!decoration-muted-foreground prose-a:underline-offset-[3px] prose-a:transition-colors hover:prose-a:!decoration-foreground prose-pre:!px-0 prose-img:mx-auto; @apply prose-headings:scroll-mt-20 prose-headings:break-words [&>section]:first:prose-headings:mt-0!;
@apply prose-p:break-words;
@apply prose-a:break-words! prose-a:decoration-muted-foreground! prose-a:underline-offset-[3px] prose-a:transition-colors prose-a:hover:decoration-foreground!;
@apply prose-pre:px-0! prose-img:mx-auto;
.katex-display { .katex-display {
@apply overflow-x-auto overflow-y-hidden py-4; @apply overflow-x-auto overflow-y-hidden py-4;
@ -94,98 +133,14 @@
@apply bg-transparent; @apply bg-transparent;
} }
/* Blanket syntax highlighting */ /* Adjacent expressive-code blocks */
code[data-theme*=' '] { div.expressive-code:has(+ div.expressive-code) {
span { @apply mb-4;
color: var(--shiki-light);
}
.dark & span {
color: var(--shiki-dark);
}
} }
/* Inline code */ /* Inline code */
:not(pre) > code { :not(pre) > code {
@apply relative rounded bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium; @apply bg-muted/50 relative rounded-sm px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium before:content-none! after:content-none!;
}
/* Code blocks */
figure[data-rehype-pretty-code-figure] {
@apply relative;
/* Code block titles */
[data-rehype-pretty-code-title] {
@apply break-words 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;
}
}
/* Shadcn-like scrollbar */
pre::-webkit-scrollbar {
@apply h-2.5 w-2.5;
}
pre::-webkit-scrollbar-track {
@apply bg-transparent;
}
pre::-webkit-scrollbar-thumb {
@apply rounded-full bg-border bg-clip-padding p-px;
}
/* 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-background p-1 backdrop-blur-none transition-all;
}
}
}
} }
} }
} }

View file

@ -1,48 +0,0 @@
import type { Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
const config: Config = {
darkMode: ['selector'],
content: ['./src/**/*.{astro,md,mdx,ts,tsx}'],
theme: {
extend: {
fontFamily: {
sans: ['Geist', ...defaultTheme.fontFamily.sans],
mono: ['Geist Mono', ...defaultTheme.fontFamily.mono],
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
additive: {
DEFAULT: 'hsl(var(--additive))',
foreground: 'hsl(var(--additive-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
border: 'hsl(var(--border))',
ring: 'hsl(var(--ring))',
},
},
},
plugins: [require('@tailwindcss/typography'), require('tailwindcss-animate')],
}
export default config