feat: upgrade to astro 5
This commit is contained in:
parent
47f21f8b3c
commit
0704481e0b
16 changed files with 3976 additions and 2671 deletions
|
@ -173,12 +173,12 @@ The blog post schema is defined as follows:
|
|||
| `date` | `coerce.date()` | Must be in `YYYY-MM-DD` format. | Yes |
|
||||
| `image` | `image()` | Must be exactly 1200px × 630px. | Optional |
|
||||
| `tags` | `string[]` | Preferably use kebab-case for these. | Optional |
|
||||
| `authors` | `string[]` | If the author has a profile, use the slug associated with their Markdown file in `src/content/authors/` (e.g. if their file is named `jane-doe.md`, use `jane-doe` in the array). | Optional |
|
||||
| `authors` | `string[]` | If the author has a profile, use the id associated with their Markdown file in `src/content/authors/` (e.g. if their file is named `jane-doe.md`, use `jane-doe` in the array). | Optional |
|
||||
| `draft` | `boolean` | Defaults to `false` if not provided. | Optional |
|
||||
|
||||
### Authors
|
||||
|
||||
Add author information in `src/content/authors/` as Markdown files. A file named `[author-name].md` can be associated with a blog post if `"author-name"` (the slug) is added to the `authors` field:
|
||||
Add author information in `src/content/authors/` as Markdown files. A file named `[author-name].md` can be associated with a blog post if `"author-name"` (the id) is added to the `authors` field:
|
||||
|
||||
```yml
|
||||
---
|
||||
|
|
|
@ -43,7 +43,6 @@ export default defineConfig({
|
|||
],
|
||||
rehypeHeadingIds,
|
||||
rehypeKatex,
|
||||
// @ts-expect-error
|
||||
sectionize,
|
||||
[
|
||||
rehypePrettyCode,
|
||||
|
|
6445
package-lock.json
generated
6445
package-lock.json
generated
File diff suppressed because it is too large
Load diff
62
package.json
62
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "astro-erudite",
|
||||
"type": "module",
|
||||
"version": "1.1.9",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
|
@ -12,50 +12,50 @@
|
|||
"prettier": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.7.0",
|
||||
"@astrojs/markdown-remark": "^5.2.0",
|
||||
"@astrojs/mdx": "^3.1.2",
|
||||
"@astrojs/react": "^3.6.2",
|
||||
"@astrojs/rss": "^4.0.7",
|
||||
"@astrojs/sitemap": "^3.1.6",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/markdown-remark": "^6.0.1",
|
||||
"@astrojs/mdx": "^4.0.3",
|
||||
"@astrojs/react": "^4.1.2",
|
||||
"@astrojs/rss": "^4.0.10",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/tailwind": "^5.1.4",
|
||||
"@hbsnow/rehype-sectionize": "^1.0.7",
|
||||
"@iconify-json/lucide": "^1.2.4",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@iconify-json/lucide": "^1.2.20",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@rehype-pretty/transformers": "^0.13.2",
|
||||
"@shikijs/transformers": "^1.16.3",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"astro": "^4.11.3",
|
||||
"astro-icon": "^1.1.1",
|
||||
"@shikijs/transformers": "^1.24.4",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"astro": "^5.1.1",
|
||||
"astro-icon": "^1.1.4",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.441.0",
|
||||
"lucide-react": "^0.469.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"rehype-katex": "^7.0.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-pretty-code": "^0.14.0",
|
||||
"remark-emoji": "^5.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-toc": "^9.0.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-astro": "^0.13.0",
|
||||
"prettier-plugin-astro-organize-imports": "^0.4.9",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14"
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-astro-organize-imports": "^0.4.11",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
|
|
|
@ -37,7 +37,7 @@ const socialLinks: SocialLink[] = [
|
|||
>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href={`/authors/${author.slug}`}
|
||||
href={`/authors/${author.id}`}
|
||||
class={cn('block', linkDisabled && 'pointer-events-none')}
|
||||
>
|
||||
<AvatarComponent
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
import AvatarComponent from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { formatDate, parseAuthors, readingTime } from '@/lib/utils'
|
||||
import { parseAuthors } from '@/lib/server-utils'
|
||||
import { formatDate, readingTime } from '@/lib/utils'
|
||||
import { Image } from 'astro:assets'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import Link from './Link.astro'
|
||||
|
@ -16,7 +17,7 @@ const { entry } = Astro.props as {
|
|||
}
|
||||
|
||||
const formattedDate = formatDate(entry.data.date)
|
||||
const readTime = readingTime(entry.body)
|
||||
const readTime = readingTime(entry.body!)
|
||||
const authors = await parseAuthors(entry.data.authors ?? [])
|
||||
---
|
||||
|
||||
|
@ -24,7 +25,7 @@ const authors = await parseAuthors(entry.data.authors ?? [])
|
|||
class="not-prose rounded-xl border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50"
|
||||
>
|
||||
<Link
|
||||
href={`/${entry.collection}/${entry.slug}`}
|
||||
href={`/${entry.collection}/${entry.id}`}
|
||||
class="flex flex-col gap-4 sm:flex-row"
|
||||
>
|
||||
{
|
||||
|
|
|
@ -3,7 +3,7 @@ import '../styles/global.css'
|
|||
import '../styles/katex.css'
|
||||
|
||||
import { SITE } from '@/consts'
|
||||
import { ViewTransitions } from 'astro:transitions'
|
||||
import { ClientRouter } from 'astro:transitions'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
|
@ -48,7 +48,7 @@ const { title, description, image = '/static/twitter-card.png' } = Astro.props
|
|||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image" content={new URL(image, Astro.url)} />
|
||||
|
||||
<ViewTransitions />
|
||||
<ClientRouter />
|
||||
|
||||
<script is:inline>
|
||||
function setDarkMode(document) {
|
||||
|
|
|
@ -9,7 +9,7 @@ const { prevPost, nextPost } = Astro.props
|
|||
|
||||
<div class="col-start-2 flex flex-col gap-4 sm:flex-row">
|
||||
<Link
|
||||
href={nextPost ? `/blog/${nextPost.slug}` : '#'}
|
||||
href={nextPost ? `/blog/${nextPost.id}` : '#'}
|
||||
class={cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'rounded-xl group flex items-center justify-start w-full sm:w-1/2 h-fit',
|
||||
|
@ -31,7 +31,7 @@ const { prevPost, nextPost } = Astro.props
|
|||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href={prevPost ? `/blog/${prevPost.slug}` : '#'}
|
||||
href={prevPost ? `/blog/${prevPost.id}` : '#'}
|
||||
class={cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'rounded-xl group flex items-center justify-end w-full sm:w-1/2 h-fit',
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { glob } from 'astro/loaders'
|
||||
import { defineCollection, z } from 'astro:content'
|
||||
|
||||
const blog = defineCollection({
|
||||
type: 'content',
|
||||
loader: glob({ pattern: '**/*.{md,mdx}', base: "./src/content/blog" }),
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
title: z
|
||||
|
@ -17,12 +18,7 @@ const blog = defineCollection({
|
|||
'Description should be 155 characters or less for optimal Open Graph display.',
|
||||
),
|
||||
date: z.coerce.date(),
|
||||
image: image()
|
||||
.refine((img) => img.width === 1200 && img.height === 630, {
|
||||
message:
|
||||
'The image must be exactly 1200px × 630px for Open Graph requirements.',
|
||||
})
|
||||
.optional(),
|
||||
image: image().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
authors: z.array(z.string()).optional(),
|
||||
draft: z.boolean().optional(),
|
||||
|
@ -30,7 +26,7 @@ const blog = defineCollection({
|
|||
})
|
||||
|
||||
const authors = defineCollection({
|
||||
type: 'content',
|
||||
loader: glob({ pattern: '**/*.{md,mdx}', base: "./src/content/authors" }),
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
pronouns: z.string().optional(),
|
||||
|
@ -46,16 +42,13 @@ const authors = defineCollection({
|
|||
})
|
||||
|
||||
const projects = defineCollection({
|
||||
type: 'content',
|
||||
loader: glob({ pattern: '**/*.{md,mdx}', base: "./src/content/projects" }),
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
image: image().refine((img) => img.width === 1200 && img.height === 630, {
|
||||
message:
|
||||
'The image must be exactly 1200px × 630px for Open Graph requirements.',
|
||||
}),
|
||||
image: image(),
|
||||
link: z.string().url(),
|
||||
}),
|
||||
})
|
|
@ -107,7 +107,7 @@ 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 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—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.
|
||||
- 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 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.
|
||||
- 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 tag will also have their own page, which lists all of the posts under that tag!
|
||||
- $\LaTeX$ is fully supported with [KaTeX](https://katex.org/):
|
||||
|
|
27
src/lib/server-utils.ts
Normal file
27
src/lib/server-utils.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { getEntry } from "astro:content"
|
||||
|
||||
export async function parseAuthors(authors: string[]) {
|
||||
if (!authors || authors.length === 0) return []
|
||||
|
||||
const parseAuthor = async (id: string) => {
|
||||
try {
|
||||
const author = await getEntry('authors', id)
|
||||
return {
|
||||
id,
|
||||
name: author?.data?.name || id,
|
||||
avatar: author?.data?.avatar || '/static/logo.png',
|
||||
isRegistered: !!author,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching author with id ${id}:`, error)
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
avatar: '/static/logo.png',
|
||||
isRegistered: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.all(authors.map(parseAuthor))
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import { getEntry } from 'astro:content'
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
|
@ -20,29 +19,3 @@ export function readingTime(html: string) {
|
|||
const readingTimeMinutes = (wordCount / 200 + 1).toFixed()
|
||||
return `${readingTimeMinutes} min read`
|
||||
}
|
||||
|
||||
export async function parseAuthors(authors: string[]) {
|
||||
if (!authors || authors.length === 0) return []
|
||||
|
||||
const parseAuthor = async (slug: string) => {
|
||||
try {
|
||||
const author = await getEntry('authors', slug)
|
||||
return {
|
||||
slug,
|
||||
name: author?.data?.name || slug,
|
||||
avatar: author?.data?.avatar || '/static/logo.png',
|
||||
isRegistered: !!author,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching author with slug ${slug}:`, error)
|
||||
return {
|
||||
slug,
|
||||
name: slug,
|
||||
avatar: '/static/logo.png',
|
||||
isRegistered: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.all(authors.map(parseAuthor))
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { type CollectionEntry, getCollection } from 'astro:content'
|
|||
export async function getStaticPaths() {
|
||||
const authors = await getCollection('authors')
|
||||
return authors.map((author) => ({
|
||||
params: { slug: author.slug },
|
||||
params: { id: author.id },
|
||||
props: { author },
|
||||
}))
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ const { author } = Astro.props
|
|||
const allPosts = await getCollection('blog')
|
||||
const authorPosts = allPosts
|
||||
.filter((post) => {
|
||||
return post.data.authors && post.data.authors.includes(author.slug)
|
||||
return post.data.authors && post.data.authors.includes(author.id)
|
||||
})
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
---
|
|
@ -7,17 +7,18 @@ import { badgeVariants } from '@/components/ui/badge'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import { formatDate, parseAuthors, readingTime } from '@/lib/utils'
|
||||
import { parseAuthors } from '@/lib/server-utils'
|
||||
import { formatDate, readingTime } from '@/lib/utils'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { Image } from 'astro:assets'
|
||||
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||
import { type CollectionEntry, getCollection, render } from 'astro:content'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = (await getCollection('blog'))
|
||||
.filter((post) => !post.data.draft)
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
params: { id: post.id },
|
||||
props: post,
|
||||
}))
|
||||
}
|
||||
|
@ -27,28 +28,28 @@ const posts = (await getCollection('blog'))
|
|||
.filter((post) => !post.data.draft)
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
|
||||
function getPostIndex(slug: string): number {
|
||||
return posts.findIndex((post) => post.slug === slug)
|
||||
function getPostIndex(id: string): number {
|
||||
return posts.findIndex((post) => post.id === id)
|
||||
}
|
||||
|
||||
function getPrevPost(slug: string): Props | null {
|
||||
const postIndex = getPostIndex(slug)
|
||||
function getPrevPost(id: string): Props | null {
|
||||
const postIndex = getPostIndex(id)
|
||||
return postIndex !== -1 && postIndex < posts.length - 1
|
||||
? posts[postIndex + 1]
|
||||
: null
|
||||
}
|
||||
|
||||
function getNextPost(slug: string): Props | null {
|
||||
const postIndex = getPostIndex(slug)
|
||||
function getNextPost(id: string): Props | null {
|
||||
const postIndex = getPostIndex(id)
|
||||
return postIndex > 0 ? posts[postIndex - 1] : null
|
||||
}
|
||||
|
||||
const currentPostSlug = Astro.params.slug
|
||||
const nextPost = getNextPost(currentPostSlug)
|
||||
const prevPost = getPrevPost(currentPostSlug)
|
||||
const currentPostId = Astro.params.id
|
||||
const nextPost = getNextPost(currentPostId)
|
||||
const prevPost = getPrevPost(currentPostId)
|
||||
|
||||
const post = Astro.props
|
||||
const { Content, headings } = await post.render()
|
||||
const { Content, headings } = await render(post)
|
||||
|
||||
const authors = await parseAuthors(post.data.authors ?? [])
|
||||
---
|
||||
|
@ -103,7 +104,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
|||
/>
|
||||
{author.isRegistered ? (
|
||||
<Link
|
||||
href={`/authors/${author.slug}`}
|
||||
href={`/authors/${author.id}`}
|
||||
underline
|
||||
class="text-foreground"
|
||||
>
|
||||
|
@ -122,7 +123,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
|||
<div class="flex items-center gap-2">
|
||||
<span>{formatDate(post.data.date)}</span>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<span>{readingTime(post.body)}</span>
|
||||
<span>{readingTime(post.body!)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
|
@ -1,5 +1,5 @@
|
|||
import rss from '@astrojs/rss'
|
||||
import { SITE } from '@/consts'
|
||||
import rss from '@astrojs/rss'
|
||||
import type { APIContext } from 'astro'
|
||||
import { getCollection } from 'astro:content'
|
||||
|
||||
|
@ -24,7 +24,7 @@ export async function GET(context: APIContext) {
|
|||
title: item.data.title,
|
||||
description: item.data.description,
|
||||
pubDate: item.data.date,
|
||||
link: `/${item.collection}/${item.slug}/`,
|
||||
link: `/${item.collection}/${item.id}/`,
|
||||
})),
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
|
@ -23,7 +23,7 @@ export async function getStaticPaths() {
|
|||
)
|
||||
|
||||
return uniqueTags.map((tag) => ({
|
||||
params: { slug: tag },
|
||||
params: { id: tag },
|
||||
props: {
|
||||
tag,
|
||||
posts: posts.filter((post) => post.data.tags?.includes(tag)),
|
Loading…
Add table
Reference in a new issue