feat(build): update to nextra v4

This commit is contained in:
Anthony Berg 2025-04-18 14:13:13 +02:00
parent c5c576cbcb
commit 5080bbff7c
23 changed files with 1885 additions and 1165 deletions

3
.gitignore vendored
View File

@ -35,5 +35,8 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
# nextra
_pagefind/
# IDEA folder
/.idea/

View File

@ -2,13 +2,15 @@ import nextra from 'nextra'
import type { NextConfig } from 'next'
const withNextra = nextra({
theme: 'nextra-theme-blog',
themeConfig: './src/theme.config.tsx',
// optional: add `unstable_staticImage: true` to enable Nextra's auto image import
// ... Other Nextra config options
search: false
})
const nextConfig: NextConfig = {
// any configs you need
turbopack: {
resolveExtensions: ['.mdx', '.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'],
},
}
export default withNextra(nextConfig)

View File

@ -6,9 +6,10 @@
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev",
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start"
"start": "next start",
"postbuild": "pagefind --site .next/server/app --output-path public/_pagefind"
},
"engines": {
"node": ">=20.x",
@ -16,16 +17,18 @@
},
"dependencies": {
"gray-matter": "^4.0.3",
"next": "^15.0.2",
"nextra": "^3.2.0",
"nextra-theme-blog": "^3.2.0",
"next": "^15.3.1",
"nextra": "^4.2.17",
"nextra-theme-blog": "^4.2.17",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/node": "^22.8.4",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"typescript": "^5.6.3"
"@types/mdx": "^2.0.13",
"@types/node": "^22.14.1",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"pagefind": "^1.3.0",
"typescript": "^5.8.3"
}
}

2606
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
import { generateStaticParamsFor, importPage } from 'nextra/pages'
import { useMDXComponents as getMDXComponents } from '../../mdx-components'
import { MDXComponents } from "mdx/types";
export const generateStaticParams = generateStaticParamsFor('mdxPath')
type Props = {
params: Promise<{
mdxPath: string[]
}>
}
export async function generateMetadata(props: Props) {
const params = await props.params
const { metadata } = await importPage(params.mdxPath)
return metadata
}
const Wrapper = getMDXComponents().wrapper
export default async function Page(props: Props) {
const params = await props.params
const result = await importPage(params.mdxPath)
const { default: MDXContent, toc, metadata } = result
return (
<Wrapper toc={toc} metadata={metadata}>
<MDXContent {...props} params={params} />
</Wrapper>
)
}

55
src/app/layout.tsx Normal file
View File

@ -0,0 +1,55 @@
import { Layout, Navbar, ThemeSwitch } from 'nextra-theme-blog';
import { Head, Search } from 'nextra/components';
import { getPageMap } from 'nextra/page-map';
import 'nextra-theme-blog/style.css';
import React from 'react';
import { Metadata } from 'next';
import Footer from '@/components/footer'
export const metadata: Metadata = {
title: {
default: 'Anthony Berg\'s Portfolio',
template: '%s | Anthony Berg',
},
description: 'My own personal portfolio including for my own projects related to computer science',
openGraph: {
url: 'https://anthonyberg.io',
siteName: 'Anthony Berg\'s Website',
locale: 'en_GB',
type: 'website',
images: [{
url: '/images/kgxtunnel.jpg',
}]
}
}
const YEAR = new Date().getFullYear()
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html
lang="en"
dir="ltr"
suppressHydrationWarning
>
<Head backgroundColor={{dark: '#0f172a', light: '#fefce8'}} />
<body>
<Layout>
<Navbar pageMap={await getPageMap()}>
{/*<Search placeholder={"Search..."}/>*/}
<ThemeSwitch />
</Navbar>
{children}
<Footer />
</Layout>
</body>
</html>
);
}

View File

@ -0,0 +1,18 @@
import { normalizePages } from 'nextra/normalize-pages'
import { getPageMap } from 'nextra/page-map'
export async function getPosts() {
const { directories } = normalizePages({
list: await getPageMap('/posts'),
route: '/posts'
})
return directories
.filter(post => post.name !== 'index')
.sort((a, b) => (new Date(b.frontMatter.date)).valueOf() - (new Date(a.frontMatter.date)).valueOf())
}
export async function getTags() {
const posts = await getPosts()
const tags = posts.flatMap(post => post.frontMatter.tags)
return tags
}

36
src/app/posts/page.tsx Normal file
View File

@ -0,0 +1,36 @@
import Link from 'next/link'
import { PostCard } from 'nextra-theme-blog'
import { getPosts, getTags } from './get-posts'
export const metadata = {
title: 'Posts'
}
export default async function PostsPage() {
const tags = await getTags()
const posts = await getPosts()
const allTags: Record<string, number> = Object.create(null)
for (const tag of tags) {
allTags[tag] ??= 0
allTags[tag] += 1
}
return (
<div data-pagefind-ignore="all">
<h1>{metadata.title}</h1>
<div
className="not-prose"
style={{ display: 'flex', flexWrap: 'wrap', gap: '.5rem' }}
>
{Object.entries(allTags).map(([tag, count]) => (
<Link key={tag} href={`/tags/${tag}`} className="nextra-tag">
{tag} ({count})
</Link>
))}
</div>
{posts.map(post => (
<PostCard key={post.route} post={post} />
))}
</div>
)
}

39
src/app/rss.xml/route.ts Normal file
View File

@ -0,0 +1,39 @@
import { getPosts } from '../posts/get-posts'
const CONFIG = {
title: 'Anthony\'s Blog',
siteUrl: 'https://anthonyberg.io',
description: 'Anthony\'s latest blog posts about tech related things.',
lang: 'en-gb'
}
export async function GET() {
const allPosts = await getPosts()
const posts = allPosts
.map(
post => ` <item>
<title>${post.title}</title>
<description>${post.frontMatter.description}</description>
<link>${CONFIG.siteUrl}${post.route}</link>
<pubDate>${new Date(post.frontMatter.date).toUTCString()}</pubDate>
</item>`
)
.join('\n')
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>${CONFIG.title}</title>
<link>${CONFIG.siteUrl}</link>
<description>${CONFIG.description}</description>
<language>${CONFIG.lang}</language>
${posts}
</channel>
</rss>`
return new Response(xml, {
headers: {
'Content-Type': 'application/rss+xml'
}
})
}

View File

@ -0,0 +1,33 @@
import { PostCard } from 'nextra-theme-blog'
import { getPosts, getTags } from '../../posts/get-posts'
export async function generateMetadata(props: { params: any }) {
const params = await props.params
return {
title: `Posts Tagged with “${decodeURIComponent(params.tag)}`
}
}
export async function generateStaticParams() {
const allTags = await getTags()
return [...new Set(allTags)].map(tag => ({ tag }))
}
export default async function TagPage(props: { params: any }) {
const params = await props.params
const { title } = await generateMetadata({ params })
const posts = await getPosts()
return (
<>
<h1>{title}</h1>
{posts
.filter(post =>
post.frontMatter.tags.includes(decodeURIComponent(params.tag))
)
.map(post => (
<PostCard key={post.route} post={post} />
))}
</>
)
}

29
src/components/footer.tsx Normal file
View File

@ -0,0 +1,29 @@
'use client';
import Link from 'next/link';
import { Footer as NXFooter } from 'nextra-theme-blog'
function Footer() {
const YEAR = new Date().getFullYear();
return (
<NXFooter>
<small>
<Link href={"https://github.com/smyalygames/anthonyberg-website/blob/main/LICENSE"}>
<time>{YEAR}</time>
© Anthony Berg.</Link>
<a href={"https://github.com/smyalygames/anthonyberg-website/"}>GitHub Repository</a>
</small>
<style jsx>{`
footer {
margin-top: 8rem;
}
a {
float: right;
}
`}</style>
</NXFooter>
);
}
export default Footer;

View File

@ -1,3 +1,5 @@
"use client"
import styles from './theme-image.module.css'
import Image, { ImageProps } from 'next/image'

24
src/content/_meta.ts Normal file
View File

@ -0,0 +1,24 @@
import type { MetaRecord } from 'nextra';
const meta: MetaRecord = {
index: {
title: 'Home',
type: 'page',
},
projects: {
title: 'Projects',
type: 'page',
},
posts: {
title: 'Blog',
type: 'page'
},
veganenumbers: {
display: 'hidden',
theme: {
sidebar: false
}
}
}
export default meta;

View File

@ -1,22 +1,18 @@
---
type: page
title: Anthony Berg
date: 2024-10-05
---
import { ThemeImage } from '@/components/theme-image'
# Anthony Berg
Hey, welcome to my website! :D
I'm Anthony, and I enjoy working with computers, be it from developing on my own projects,
or just using Linux on my PCs or setting up my own personal servers.
For my day to day life, I am currently in my 1<sup>st</sup> year of my Master's at OsloMet, studying
For my day-to-day life, I am currently in my 1<sup>st</sup> year of my Master's at OsloMet, studying
Applied Computer and Information Technology, specialising in Mathematical Modelling and Scientific
Computing.
I have also completed BSc Computer Science at Newcastle University.
I have also completed B.Sc. Computer Science at Newcastle University.
I have a few open source projects that I have worked on that you can find in my [portfolio](/projects).

View File

@ -1,13 +1,9 @@
---
type: page
title: Projects
date: 2023-11-14
---
import Image from "next/image";
# Projects
Here are some of my projects that I have worked on

13
src/mdx-components.ts Normal file
View File

@ -0,0 +1,13 @@
import { useMDXComponents as getThemeComponents } from 'nextra-theme-blog';
import type { MDXComponents } from 'mdx/types';
// Get the default MDX components
const themeComponents = getThemeComponents();
// Merge components
export function useMDXComponents(components?: MDXComponents) {
return {
...themeComponents,
...components
}
}

View File

@ -1,6 +0,0 @@
import type { AppProps } from 'next/app'
import '../styles/main.css'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}

View File

@ -1,9 +0,0 @@
---
type: posts
title: Posts
date: 2023-11-14
---
# Posts
## This is currently under construction

View File

@ -1,13 +0,0 @@
---
type: tag
title: Tagged Posts
---
import { useRouter } from 'next/router'
export const TagName = () => {
const { tag } = useRouter().query
return tag || null
}
# Posts Tagged with “<TagName/>”

View File

@ -1,57 +0,0 @@
import Link from "next/link";
const YEAR = new Date().getFullYear()
const websiteMeta = {
title: 'Anthony Berg\'s Portfolio',
description: 'My own personal portfolio including for my own projects related to computer science',
image: '/images/kgxtunnel.jpg',
}
export default {
footer: (
<footer>
<small>
<Link href={"https://github.com/smyalygames/anthonyberg-website/blob/main/LICENSE"}><time>{YEAR}</time> © Anthony Berg.</Link>
<a href={"https://github.com/smyalygames/anthonyberg-website/"}>GitHub Repository</a>
</small>
<style jsx>{`
footer {
margin-top: 8rem;
}
a {
float: right;
}
`}</style>
</footer>
),
head: ({ title, meta } : { title: string; meta: any }) => (
<>
<link
rel="preload"
href="/fonts/Inter-roman.latin.var.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
{meta.description && (
<meta name="description" content={meta.description}/>
)}
{meta.tag && <meta name="keywords" content={meta.tag}/>}
{meta.author && <meta name="author" content={meta.author}/>}
{/*og meta*/}
<meta name="robots" content="follow, index"/>
<meta name="description" content={websiteMeta.description}/>
<meta property="og:site_name" content={websiteMeta.title}/>
<meta property="og:description" content={websiteMeta.description}/>
<meta property="og:title" content={websiteMeta.title}/>
<meta property="og:image" content={websiteMeta.image}/>
<meta name="twitter:card" content="summary_large_image"/>
{/*<meta name="twitter:site" content="@yourname" />*/}
<meta name="twitter:title" content={websiteMeta.title}/>
<meta name="twitter:description" content={websiteMeta.description}/>
<meta name="twitter:image" content={websiteMeta.image}/>
</>
)
}

View File

@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"target": "es2020",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@ -9,15 +13,29 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/components/*": ["./src/components/*"]
}
"@/components/*": [
"./src/components/*"
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}