Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 1 addition & 26 deletions app/posts/[slug]/actions.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import * as postsActions from '../actions'

import { fetchPostBySlug, fetchPreviousPost } from './actions'
import { fetchPostBySlug } from './actions'

import * as mdx from '@/lib/mdx'
import type { Post } from '@/lib/types'
import { getMockFrontmatter } from '@/test/mocks/frontmatter'
import { getMockSource } from '@/test/mocks/source'

vi.mock('@/lib/mdx')
vi.mock('../actions')
Expand Down Expand Up @@ -56,26 +53,4 @@ describe('/posts/[slug]/actions', () => {
})
})
})

describe('fetchPreviousPost', () => {
const mockPosts: Post[] = [
{ slug: 'slug-1', ...getMockFrontmatter(), source: getMockSource() },
{ slug: 'slug-2', ...getMockFrontmatter(), source: getMockSource() },
]

it('returns the next index of results returned from fetchPublishedPosts()', async () => {
vi.mocked(postsActions.fetchPublishedPosts).mockResolvedValue(mockPosts)

const actual = await fetchPreviousPost('slug-1')
expect(actual).toBeTruthy()
expect(actual?.slug).toEqual('slug-2')
})

it('returns undefined when the bottom of the list is reached', async () => {
vi.mocked(postsActions.fetchPublishedPosts).mockResolvedValue(mockPosts)

const actual = await fetchPreviousPost('slug-2')
expect(actual).toBeUndefined()
})
})
})
11 changes: 0 additions & 11 deletions app/posts/[slug]/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import path from 'path'

import { notFound } from 'next/navigation'

import { fetchPublishedPosts } from '../actions'

import { getPostFromMDX } from '@/lib/mdx'
import type { Post } from '@/lib/types'

Expand All @@ -22,12 +20,3 @@ export async function fetchPostBySlug(slug: string): Promise<Post> {
}
}
}

export async function fetchPreviousPost(slug: string): Promise<Post | undefined> {
const publishedPosts = await fetchPublishedPosts()
const postIndex = publishedPosts.findIndex((post) => post.slug === slug)

if (postIndex === publishedPosts.length - 1) return undefined

return publishedPosts[postIndex + 1]
}
68 changes: 0 additions & 68 deletions app/posts/[slug]/mdx-content.tsx

This file was deleted.

27 changes: 10 additions & 17 deletions app/posts/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { Metadata } from 'next'
import { ChevronLeft } from 'lucide-react'
import { type Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'

import { fetchPublishedPosts } from '../actions'

import { fetchPostBySlug, fetchPreviousPost } from './actions'
import MDXContent from './mdx-content'
import { fetchPostBySlug } from './actions'
import TimeInformation from './time-information'

import { Tag } from '@/components/tag'
Expand All @@ -19,11 +18,13 @@
export async function generateStaticParams() {
const publishedPosts = await fetchPublishedPosts()

return publishedPosts.map((post) => ({
slug: post.slug,
return publishedPosts.map(({ slug }) => ({
slug,
}))
}

export const dynamicParams = false

export const generateMetadata = async ({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> => {
const { slug } = await params
const post = await fetchPostBySlug(slug)
Expand All @@ -50,13 +51,14 @@
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await fetchPostBySlug(slug)
const previousPost = await fetchPreviousPost(slug)

const { default: Post } = await import(`@/posts/${slug}.mdx`)

return (
<div className="mx-0 my-10 flex flex-col items-center md:mx-20">
<AspectRatio ratio={16 / 9}>
<Image src={post.thumbnail} alt="Article cover image" className="rounded-xl border-2" priority fill />
</AspectRatio>

Check failure on line 61 in app/posts/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / test

app/posts/[slug]/page.test.tsx > posts/[slug]/PostPage > renders a link to the previous blog post

Error: Cannot find package '@/posts/my-slug.mdx' imported from '/home/runner/work/jameswalsh.dev/jameswalsh.dev/app/posts/[slug]/page.tsx' ❯ Module.PostPage app/posts/[slug]/page.tsx:61:7 ❯ app/posts/[slug]/page.test.tsx:50:12 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Error: Failed to load url @/posts/my-slug.mdx (resolved id: @/posts/my-slug.mdx). Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@6.3.5_@types+node@22.7.5_jiti@2.4.2_lightningcss@1.30.1_yaml@2.8.0/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:35725:17

Check failure on line 61 in app/posts/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / test

app/posts/[slug]/page.test.tsx > posts/[slug]/PostPage > renders a link to all posts

Error: Cannot find package '@/posts/my-slug.mdx' imported from '/home/runner/work/jameswalsh.dev/jameswalsh.dev/app/posts/[slug]/page.tsx' ❯ Module.PostPage app/posts/[slug]/page.tsx:61:7 ❯ app/posts/[slug]/page.test.tsx:50:12 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Error: Failed to load url @/posts/my-slug.mdx (resolved id: @/posts/my-slug.mdx). Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@6.3.5_@types+node@22.7.5_jiti@2.4.2_lightningcss@1.30.1_yaml@2.8.0/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:35725:17

Check failure on line 61 in app/posts/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / test

app/posts/[slug]/page.test.tsx > posts/[slug]/PostPage > renders blog time information

Error: Cannot find package '@/posts/my-slug.mdx' imported from '/home/runner/work/jameswalsh.dev/jameswalsh.dev/app/posts/[slug]/page.tsx' ❯ Module.PostPage app/posts/[slug]/page.tsx:61:7 ❯ app/posts/[slug]/page.test.tsx:50:12 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Error: Failed to load url @/posts/my-slug.mdx (resolved id: @/posts/my-slug.mdx). Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@6.3.5_@types+node@22.7.5_jiti@2.4.2_lightningcss@1.30.1_yaml@2.8.0/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:35725:17

Check failure on line 61 in app/posts/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / test

app/posts/[slug]/page.test.tsx > posts/[slug]/PostPage > renders all the blog tags

Error: Cannot find package '@/posts/my-slug.mdx' imported from '/home/runner/work/jameswalsh.dev/jameswalsh.dev/app/posts/[slug]/page.tsx' ❯ Module.PostPage app/posts/[slug]/page.tsx:61:7 ❯ app/posts/[slug]/page.test.tsx:50:12 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Error: Failed to load url @/posts/my-slug.mdx (resolved id: @/posts/my-slug.mdx). Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@6.3.5_@types+node@22.7.5_jiti@2.4.2_lightningcss@1.30.1_yaml@2.8.0/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:35725:17

Check failure on line 61 in app/posts/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / test

app/posts/[slug]/page.test.tsx > posts/[slug]/PostPage > renders the blog thumbnail image

Error: Cannot find package '@/posts/my-slug.mdx' imported from '/home/runner/work/jameswalsh.dev/jameswalsh.dev/app/posts/[slug]/page.tsx' ❯ Module.PostPage app/posts/[slug]/page.tsx:61:7 ❯ app/posts/[slug]/page.test.tsx:50:12 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Error: Failed to load url @/posts/my-slug.mdx (resolved id: @/posts/my-slug.mdx). Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@6.3.5_@types+node@22.7.5_jiti@2.4.2_lightningcss@1.30.1_yaml@2.8.0/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:35725:17

Check failure on line 61 in app/posts/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / test

app/posts/[slug]/page.test.tsx > posts/[slug]/PostPage > renders H1 for blog post

Error: Cannot find package '@/posts/my-slug.mdx' imported from '/home/runner/work/jameswalsh.dev/jameswalsh.dev/app/posts/[slug]/page.tsx' ❯ Module.PostPage app/posts/[slug]/page.tsx:61:7 ❯ app/posts/[slug]/page.test.tsx:50:12 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Caused by: Error: Failed to load url @/posts/my-slug.mdx (resolved id: @/posts/my-slug.mdx). Does the file exist? ❯ loadAndTransform node_modules/.pnpm/vite@6.3.5_@types+node@22.7.5_jiti@2.4.2_lightningcss@1.30.1_yaml@2.8.0/node_modules/vite/dist/node/chunks/dep-DBxKXgDP.js:35725:17
<TypographyH1>{post.title}</TypographyH1>
<div className="flex w-full flex-col items-center gap-4">
<span className="flex flex-row flex-wrap gap-2">
Expand All @@ -67,22 +69,13 @@
<TimeInformation metadata={{ publishedAt: post.publishedAt, source: post.source }} />
</div>
<article className="mt-8">
<MDXContent source={post.source} />
<Post />
</article>
<div className="border-color mt-8 flex w-full flex-row justify-center gap-4 border-t pt-8">
<Link href="/posts" className={cn(buttonVariants({ variant: 'outline' }), 'w-2/5 md:w-1/2')}>
<ChevronLeft width={16} height={16} />
&nbsp;All posts
</Link>
{!!previousPost && (
<Link
href={`/posts/${previousPost.slug}`}
className={cn(buttonVariants({ variant: 'outline' }), 'w-2/5 md:w-1/2')}
>
<ChevronRight width={16} height={16} />
&nbsp;Next
</Link>
)}
</div>
</div>
)
Expand Down
39 changes: 39 additions & 0 deletions mdx-components.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { MDXComponents } from 'mdx/types'
import Image, { type ImageProps } from 'next/image'
import type { HTMLAttributes, PropsWithChildren } from 'react'

export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...{
img: (props: HTMLAttributes<HTMLImageElement>) => (
<Image
{...(props as ImageProps)}
alt="article image"
className="my-4 aspect-video rounded-lg object-center"
width={1600}
height={900}
/>
),
h1: (props: PropsWithChildren) => <h1 className="prose my-8 text-4xl font-semibold md:my-12" {...props} />,
h2: (props: PropsWithChildren) => <h2 className="prose my-6 text-3xl font-semibold md:my-8" {...props} />,
h3: (props: PropsWithChildren) => <h3 className="prose my-6 text-2xl font-semibold md:my-8" {...props} />,
p: (props: PropsWithChildren) => <h4 className="text-normal w-prose my-2 font-normal" {...props} />,
ol: (props: PropsWithChildren) => <ol className="my-6 ml-6 list-decimal [&>li]:mt-2" {...props} />,
ul: (props: PropsWithChildren) => <ul className="my-6 ml-6 list-disc [&>li]:mt-2" {...props} />,
blockquote: (props: PropsWithChildren) => <blockquote className="my-6 border-l-2 pl-6 italic" {...props} />,
code: (props: PropsWithChildren) => (
<code
className="bg-muted relative rounded-lg px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold"
{...props}
/>
),
pre: ({ className, ...rest }: HTMLAttributes<HTMLElement>) => (
<pre className="my-5 whitespace-pre-wrap [&>code:nth-child(1)]:p-3" {...rest} />
),
a: (props: PropsWithChildren) => (
<a className="text-md text-primary/75 p-0 underline underline-offset-2" {...props} />
),
},
...components,
}
}
24 changes: 0 additions & 24 deletions next.config.mjs

This file was deleted.

59 changes: 59 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import createMDX from '@next/mdx'
import { type NextConfig } from 'next'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypePrettyCode from 'rehype-pretty-code'
import rehypeSlug from 'rehype-slug'
import remarkFrontmatter from 'remark-frontmatter'
import remarkGfm from 'remark-gfm'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
import { type HighlighterCoreOptions, getSingletonHighlighter } from 'shiki'

const nextConfig: NextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [],
},
transpilePackages: ['next-mdx-remote'],
pageExtensions: ['md', 'mdx', 'ts', 'tsx'],
async rewrites() {
return [
{
source: '/ingest/static/:path*',
destination: 'https://us-assets.i.posthog.com/static/:path*',
},
{
source: '/ingest/:path*',
destination: 'https://us.i.posthog.com/:path*',
},
]
},
// ? NOTE: This is required to support PostHog trailing slash API requests
skipTrailingSlashRedirect: true,
}

const withMDX = createMDX({
extension: /\.(md|mdx)$/,
options: {
remarkPlugins: [remarkGfm, remarkFrontmatter, remarkMdxFrontmatter],
rehypePlugins: [
rehypeSlug,
rehypeAutolinkHeadings,
[
rehypePrettyCode,
{
theme: 'one-dark-pro',
keepBackground: false,
getHighlighter: (options: HighlighterCoreOptions) => {
return getSingletonHighlighter({
...options,
themes: ['one-dark-pro'],
langs: ['js', 'ts', 'jsx', 'tsx', 'json', 'json5', 'shell', 'bash', 'astro', 'markdown'],
})
},
},
],
],
},
})

export default withMDX(nextConfig)
15 changes: 12 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
}
},
"lint-staged": {
"*.{ts,tsx}": ["next lint --fix", "pnpm format"],
"*.{ts,tsx}": [
"next lint --fix",
"pnpm format"
],
"*.md": "prettier --list-different"
},
"scripts": {
Expand All @@ -39,6 +42,9 @@
"typecheck": "tsc --pretty"
},
"dependencies": {
"@mdx-js/loader": "3.1.0",
"@mdx-js/react": "3.1.0",
"@next/mdx": "15.3.5",
"@radix-ui/react-accordion": "1.2.11",
"@radix-ui/react-aspect-ratio": "1.1.7",
"@radix-ui/react-avatar": "1.1.10",
Expand All @@ -56,15 +62,13 @@
"geist": "1.4.2",
"lucide-react": "0.511.0",
"next": "15.3.3",
"next-mdx-remote": "5.0.0",
"next-themes": "0.4.6",
"posthog-js": "1.255.0",
"posthog-node": "4.17.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"rss": "1.2.2",
"sharp": "0.34.2",
"shiki": "3.4.2",
"tailwind-merge": "3.3.1"
},
"devDependencies": {
Expand All @@ -74,9 +78,11 @@
"@next/eslint-plugin-next": "15.3.4",
"@playwright/test": "1.53.0",
"@tailwindcss/postcss": "4.1.11",
"@tailwindcss/typography": "0.5.16",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/mdx": "2.0.13",
"@types/node": "22.7.5",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.5",
Expand Down Expand Up @@ -104,7 +110,10 @@
"rehype-autolink-headings": "7.1.0",
"rehype-pretty-code": "0.14.1",
"rehype-slug": "6.0.0",
"remark-frontmatter": "5.0.0",
"remark-gfm": "4.0.1",
"remark-mdx-frontmatter": "5.2.0",
"shiki": "3.4.2",
"tailwindcss": "4.1.10",
"tailwindcss-animate": "1.0.7",
"tw-animate-css": "1.3.0",
Expand Down
Loading
Loading