Lil bit of cleanup

This commit is contained in:
HangerThem 2026-08-03 12:33:39 +02:00
parent 722ace7c74
commit 1a713fe1b6
18 changed files with 304 additions and 205 deletions

View file

@ -16,6 +16,13 @@ export async function generateMetadata({ params }: CategoryPageProps): Promise<M
return { return {
title: `${category} | ${metadata.title!.toString()}`, title: `${category} | ${metadata.title!.toString()}`,
description: 'List of posts in this category', description: 'List of posts in this category',
openGraph: {
type: 'website',
title: `${category} | ${metadata.title!.toString()}`,
description: 'List of posts in this category',
url: process.env.SITE_URL + `/categories/${categorySlug}`,
siteName: metadata.title!.toString(),
},
} }
} }

View file

@ -0,0 +1,77 @@
import { getAllCategories } from '@/lib/categories'
import { getAllPostsMeta } from '@/lib/posts'
import { slugify } from '@/utils/slug'
import type { Metadata } from 'next'
import Link from 'next/link'
import { metadata } from '@/app/metadata'
import { dateFormatter } from '@/utils/time'
import { RecentlyUpdatedPosts } from '@/components/common/RecentlyUpdatedPosts'
export async function generateMetadata(): Promise<Metadata> {
return {
title: `Categories | ${metadata.title!.toString()}`,
description: 'List of categories',
openGraph: {
type: 'website',
title: `Categories | ${metadata.title!.toString()}`,
description: 'List of categories',
url: process.env.SITE_URL + '/categories',
siteName: metadata.title!.toString(),
},
}
}
export default function CategoriesPage() {
const categories = getAllCategories()
const posts = getAllPostsMeta()
const categoryNames = Object.keys(categories)
return (
<main className="max-w-3xl mx-auto px-4 py-8 lg:py-16 w-full">
<div className="text-sm text-fg-dim mb-2">~/categories</div>
<ul className="border-l border-neutral-800 ml-1">
{categoryNames.map((category) => {
const categoryPosts = posts.filter((post) => post.category === category)
return (
<li key={category} className="pl-5">
<Link
href={`/categories/${slugify(category)}`}
className="group flex items-baseline gap-2 py-2 px-2 -ml-2 hover:bg-accent/10 transition-colors"
>
<span className="font-bold group-hover:text-accent transition-colors">
{category}
</span>
<span className="font-mono text-xs text-neutral-600">({categoryPosts.length})</span>
</Link>
<ul className="border-l border-neutral-800 ml-3 mb-3">
{categoryPosts.map((post) => (
<li key={post.slug} className="pl-5">
<Link
href={`/posts/${post.slug}`}
className="group flex items-baseline justify-between gap-3 py-1.5 px-2 -ml-2 border-l-2 border-transparent hover:bg-accent/10 hover:border-accent transition-colors"
>
<span className="group-hover:text-accent transition-colors truncate">
{post.title}
</span>
{post.date && (
<time
dateTime={new Date(post.date).toISOString()}
className="text-sm whitespace-nowrap"
>
{dateFormatter.format(new Date(post.date))}
</time>
)}
</Link>
</li>
))}
</ul>
</li>
)
})}
</ul>
</main>
)
}

View file

@ -0,0 +1,10 @@
import { RecentlyUpdatedPosts } from '@/components/common/RecentlyUpdatedPosts'
export default function WithRecentLayout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<RecentlyUpdatedPosts />
</>
)
}

41
app/(withRecent)/page.tsx Normal file
View file

@ -0,0 +1,41 @@
import { getAllPostsMeta } from '@/lib/posts'
import { Post } from '@/components/common/Post'
export default function BlogIndex() {
const posts = getAllPostsMeta()
const rest = posts.slice(1)
const rows = []
for (let i = 0; i < rest.length; i += 2) {
rows.push(rest.slice(i, i + 2))
}
return (
<main className="max-w-3xl mx-auto px-4 py-8 lg:py-16">
<section>
{posts.length === 0 ? (
<div className="rounded-xl border border-border p-6 text-fg-dim">
No published posts yet.
</div>
) : (
<>
<Post post={posts[0]} isLatest />
<div className="border-t border-border my-6" aria-hidden="true"></div>
<div className="flex flex-col gap-8 mt-4">
{rows.map((row) => {
const rowHasImage = row.some((p) => p.coverImage)
return (
<div key={row[0].slug} className="grid grid-cols-1 gap-4 md:grid-cols-2">
{row.map((post) => (
<Post post={post} key={post.slug} showImageSlot={rowHasImage} />
))}
</div>
)
})}
</div>
</>
)}
</section>
</main>
)
}

View file

@ -1,43 +0,0 @@
import { getAllCategories } from '@/lib/categories'
import { getAllPostsMeta } from '@/lib/posts'
import { slugify } from '@/utils/slug'
import type { Metadata } from 'next'
import Link from 'next/link'
import { metadata } from '@/app/metadata'
export async function generateMetadata(): Promise<Metadata> {
return { title: `Categories | ${metadata.title!.toString()}`, description: 'List of categories' }
}
export default function CategoriesPage() {
const categories = getAllCategories()
const posts = getAllPostsMeta()
return (
<div className="flex w-full">
<main className="max-w-3xl mx-auto px-4 py-8 lg:py-16 w-full">
<h1 className="text-3xl font-bold">Categories</h1>
<ul>
{Object.keys(categories).map((category) => (
<li key={category}>
<Link href={`/categories/${slugify(category)}`}>
{category} ({categories[category]})
</Link>
<ul>
{posts
.filter((post) => post.category === category)
.map((post) => (
<li key={post.slug}>
<Link href={`/posts/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
</li>
))}
</ul>
</main>
<div className="w-80" />
</div>
)
}

View file

@ -43,7 +43,8 @@ body {
@apply list-decimal; @apply list-decimal;
} }
ul, ol { ul,
ol {
@apply list-outside; @apply list-outside;
} }
@ -151,7 +152,7 @@ dd {
} }
.prose a { .prose a {
@apply text-accent; @apply text-accent hover:underline focus:underline cursor-pointer;
} }
.prose ul:not(.task-list-container) { .prose ul:not(.task-list-container) {

14
app/metadata.ts Normal file
View file

@ -0,0 +1,14 @@
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Ephemeris",
description: "A blog about me and my interests",
creator: "hangerthem",
openGraph: {
type: "website",
title: "Ephemeris",
description: "A blog about me and my interests",
url: process.env.SITE_URL,
siteName: "Ephemeris",
},
}

View file

@ -1,7 +0,0 @@
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Ephemeris",
description: "A blog about me and my interests",
creator: "hangerthem",
}

View file

@ -1,63 +0,0 @@
import { getAllPostsMeta } from '@/lib/posts'
import { Post } from '@/components/common/Post'
import Link from 'next/link'
export default function BlogIndex() {
const posts = getAllPostsMeta()
const rest = posts.slice(1)
const rows = []
for (let i = 0; i < rest.length; i += 2) {
rows.push(rest.slice(i, i + 2))
}
return (
<>
<main className="max-w-3xl mx-auto px-4 py-8 lg:py-16">
<section>
{posts.length === 0 ? (
<div className="rounded-xl border border-border p-6 text-fg-dim">
No published posts yet.
</div>
) : (
<>
<Post post={posts[0]} isLatest />
<div className="border-t border-border my-6" aria-hidden="true"></div>
<div className="flex flex-col gap-8 mt-4">
{rows.map((row) => {
const rowHasImage = row.some((p) => p.coverImage)
return (
<div key={row[0].slug} className="grid grid-cols-1 gap-4 md:grid-cols-2">
{row.map((post) => (
<Post post={post} key={post.slug} showImageSlot={rowHasImage} />
))}
</div>
)
})}
</div>
</>
)}
</section>
</main>
<aside className="w-80 hidden lg:block px-4 py-8 lg:py-16">
<div className="sticky top-8">
<h2 className="text-xl font-bold mb-4">Recently updated</h2>
<ul className="flex flex-col gap-2 ml-2">
{posts
.toSorted(
(a, b) =>
new Date(b.updated ?? b.date).getTime() - new Date(a.updated ?? a.date).getTime(),
)
.map((post) => (
<li key={post.slug} className="text-sm text-fg-dim">
<Link href={`/posts/${post.slug}`} className="hover:underline">
{post.title}
</Link>
</li>
))}
</ul>
</div>
</aside>
</>
)
}

View file

@ -2,7 +2,8 @@ import { metadata } from '@/app/metadata'
import { ImageWrapper } from '@/components/common/ImageWrapper' import { ImageWrapper } from '@/components/common/ImageWrapper'
import { ShareLinks } from '@/components/common/ShareLinks' import { ShareLinks } from '@/components/common/ShareLinks'
import { TableOfContents } from '@/components/common/TableOfContents' import { TableOfContents } from '@/components/common/TableOfContents'
import { getAllPostSlugs, getPostBySlug } from '@/lib/posts' import { getAllPostSlugs, getNextAndPreviousPosts, getPostBySlug } from '@/lib/posts'
import { slugify } from '@/utils/slug'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import Link from 'next/link' import Link from 'next/link'
@ -19,19 +20,33 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { slug } = await params const { slug } = await params
const { meta } = await getPostBySlug(slug) const { meta } = await getPostBySlug(slug)
return { title: `${meta.title} | ${metadata.title!.toString()}`, description: meta.description } return {
title: `${meta.title} | ${metadata.title!.toString()}`,
description: meta.description,
openGraph: {
type: 'article',
title: `${meta.title} | ${metadata.title!.toString()}`,
description: meta.description,
url: process.env.SITE_URL + `/posts/${slug}`,
images: meta.coverImage ? [meta.coverImage] : undefined,
tags: meta.tags,
...(meta.updated && { modifiedTime: new Date(meta.updated).toISOString() }),
siteName: metadata.title!.toString(),
},
}
} }
export default async function Post({ params }: { params: Promise<{ slug: string }> }) { export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params const { slug } = await params
const { meta, content, tableOfContents } = await getPostBySlug(slug) const { meta, content, tableOfContents } = await getPostBySlug(slug)
const { next, previous } = getNextAndPreviousPosts(slug)
return ( return (
<main className="flex w-full"> <main className="flex w-full">
<article className="max-w-3xl mx-auto px-4 py-8 lg:py-16"> <article className="max-w-3xl mx-auto px-4 py-8 lg:py-16">
{meta.category && ( {meta.category && (
<Link <Link
href={`/categories/${meta.category.toLowerCase()}`} href={`/categories/${slugify(meta.category)}`}
className="text-accent hover:underline font-sm mb-2 block" className="text-accent hover:underline font-sm mb-2 block"
> >
{meta.category} {meta.category}
@ -99,6 +114,29 @@ export default async function Post({ params }: { params: Promise<{ slug: string
description={meta.description} description={meta.description}
/> />
</div> </div>
<div className="mt-8 flex justify-between">
{previous ? (
<Link
href={`/posts/${previous.slug}`}
className="text-accent hover:underline font-sm mb-2 block"
>
&larr; {previous.title}
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/posts/${next.slug}`}
className="text-accent hover:underline font-sm mb-2 block"
>
{next.title} &rarr;
</Link>
) : (
<div />
)}
</div>
</article> </article>
<aside className="hidden lg:block w-80"> <aside className="hidden lg:block w-80">

View file

@ -1,22 +1,11 @@
'use client' 'use client'
import { isValidElement, type ReactNode } from 'react'
import { CopyButton } from './CopyButton' import { CopyButton } from './CopyButton'
import { getTextContent } from '@/lib/react-node-text'
type CodeWrapperProps = { type CodeWrapperProps = {
language?: string language?: string
children?: ReactNode children?: React.ReactNode
}
function getTextContent(node: ReactNode): string {
if (node === null || typeof node === 'boolean') return ''
if (typeof node === 'string' || typeof node === 'number') return String(node)
if (Array.isArray(node)) return node.map(getTextContent).join('')
if (isValidElement(node)) {
const props = node.props as { children?: ReactNode }
return getTextContent(props.children)
}
return ''
} }
export function CodeWrapper({ language, children }: CodeWrapperProps) { export function CodeWrapper({ language, children }: CodeWrapperProps) {

View file

@ -1,20 +1,12 @@
import type { AnchorHTMLAttributes } from 'react'; import type { AnchorHTMLAttributes } from 'react'
import { cn } from '@/utils/cn';
type ExternalLinkProps = Omit< type ExternalLinkProps = Omit<
AnchorHTMLAttributes<HTMLAnchorElement>, AnchorHTMLAttributes<HTMLAnchorElement>,
'href' | 'rel' | 'target' 'href' | 'rel' | 'target'
> & { > & {
href: string; href: string
}; }
export function ExternalLink({ className, ...props }: ExternalLinkProps) { export function ExternalLink(props: ExternalLinkProps) {
return ( return <a {...props} target="_blank" rel="noopener noreferrer" />
<a
{...props}
className={cn(className, 'hover:underline focus:underline')}
target="_blank"
rel="noopener noreferrer"
/>
);
} }

View file

@ -0,0 +1,27 @@
import { getAllPostsMeta } from '@/lib/posts'
import Link from 'next/link'
export function RecentlyUpdatedPosts() {
const posts = getAllPostsMeta()
return (
<aside className="w-80 hidden lg:block py-4 lg:py-12">
<div className="sticky top-8 border-l border-border h-fit pl-4 py-4">
<h2 className="text-xl font-bold mb-4">Recently updated</h2>
<ul className="flex flex-col gap-2 ml-1">
{posts
.toSorted(
(a, b) =>
new Date(b.updated ?? b.date).getTime() - new Date(a.updated ?? a.date).getTime(),
)
.map((post) => (
<li key={post.slug} className="text-sm text-fg-dim">
<Link href={`/posts/${post.slug}`} className="hover:underline">
{post.title}
</Link>
</li>
))}
</ul>
</div>
</aside>
)
}

View file

@ -1,16 +1,16 @@
'use client' 'use client'
import { cn } from '@/utils/cn' import { cn } from '@/utils/cn'
import { Folder, Home, Info, Rss, Tags } from 'lucide-react' import { Folder, GitBranch, Home, Info, Rss, Tags } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { ExternalLink } from '../common/ExternalLink'
type NavItem = { type NavItem = {
href: string href: string
label: string label: string
icon: React.ReactNode icon: React.ReactNode
target?: string isExternal?: boolean
rel?: string
} }
const navItems: NavItem[] = [ const navItems: NavItem[] = [
@ -22,8 +22,13 @@ const navItems: NavItem[] = [
href: '/feed.xml', href: '/feed.xml',
label: 'RSS Feed', label: 'RSS Feed',
icon: <Rss className="w-4 h-4" />, icon: <Rss className="w-4 h-4" />,
target: '_blank', isExternal: true,
rel: 'noopener noreferrer', },
{
href: 'https://git.hangerthem.com/hangerthem/blog.hangerthem.com',
label: 'Source Code',
icon: <GitBranch className="w-4 h-4" />,
isExternal: true,
}, },
] ]
@ -45,15 +50,20 @@ export function NavLinks() {
isActive(item.href) ? 'text-fg bg-fg/25' : 'text-fg-dim hover:text-fg hover:bg-fg/10', isActive(item.href) ? 'text-fg bg-fg/25' : 'text-fg-dim hover:text-fg hover:bg-fg/10',
)} )}
> >
<Link {item.isExternal ? (
<ExternalLink
href={item.href} href={item.href}
className="flex items-center gap-2 px-2 py-1" className="flex items-center gap-2 px-2 py-1 hover:no-underline focus:no-underline"
target={item.target}
rel={item.rel}
> >
{item.icon} {item.icon}
{item.label} {item.label}
</ExternalLink>
) : (
<Link href={item.href} className="flex items-center gap-2 px-2 py-1">
{item.icon}
{item.label}
</Link> </Link>
)}
</li> </li>
)) ))
} }

View file

@ -3,7 +3,7 @@ title: Self-Development and Gamification of Life
description: In this post, I explore the concept of self-development and how gamification can be used to enhance productivity and personal growth. Learn how to turn your life into a game and level up your skills! description: In this post, I explore the concept of self-development and how gamification can be used to enhance productivity and personal growth. Learn how to turn your life into a game and level up your skills!
author: hangerthem author: hangerthem
date: 2024-08-05 22:00:00 +0200 date: 2024-08-05 22:00:00 +0200
category: "Personal Development" category: Personal Development
tags: [self-development, gamification, productivity, growth] tags: [self-development, gamification, productivity, growth]
math: true math: true
mermaid: true mermaid: true

View file

@ -72,8 +72,9 @@ $$
--- ---
[this is a link](https://example.com) this is an [external link](https://example.com)
and this is an [internal link](/about)
```mermaid ```mermaid
graph TD; graph TD;

View file

@ -18,27 +18,18 @@ import { ExternalLink } from '@/components/common/ExternalLink'
import readingTime from 'reading-time' import readingTime from 'reading-time'
import { rehypeUnwrapImages } from './rehype-unwrap-images' import { rehypeUnwrapImages } from './rehype-unwrap-images'
import { rehypeMermaidSsg } from './rehype-mermaid-ssg' import { rehypeMermaidSsg } from './rehype-mermaid-ssg'
import { stripLeadingMatch } from './react-node-text' import { getTextContent, stripLeadingMatch } from './react-node-text'
import { BlockquoteWrapper } from '@/components/common/BlockquoteWrapper' import { BlockquoteWrapper } from '@/components/common/BlockquoteWrapper'
import type { NoteType } from '@/types/Note.type' import type { NoteType } from '@/types/Note.type'
import { rehypeMark } from './rehype-mark' import { rehypeMark } from './rehype-mark'
import { rehypeDefinition } from './rehype-definition' import { rehypeDefinition } from './rehype-definition'
import { slugify } from '@/utils/slug' import { slugify } from '@/utils/slug'
import { PostMeta } from '@/types/Post.type' import { PostMeta } from '@/types/Post.type'
import Link from 'next/link'
import { ExternalLinkIcon } from 'lucide-react'
const postsDirectory = path.join(process.cwd(), 'content/posts') const postsDirectory = path.join(process.cwd(), 'content/posts')
function getTextContent(node: ReactNode): string {
if (typeof node === 'string') return node
if (typeof node === 'number') return String(node)
if (Array.isArray(node)) return node.map(getTextContent).join('')
if (node && typeof node === 'object' && 'props' in node) {
const props = node.props as { children?: ReactNode }
return getTextContent(props.children)
}
return ''
}
export function getAllPostSlugs(): string[] { export function getAllPostSlugs(): string[] {
return fs return fs
.readdirSync(postsDirectory) .readdirSync(postsDirectory)
@ -60,6 +51,17 @@ export function getAllPostsMeta(): PostMeta[] {
.toSorted((a, b) => (a.date < b.date ? 1 : -1)) .toSorted((a, b) => (a.date < b.date ? 1 : -1))
} }
export function getNextAndPreviousPosts(slug: string): {
next: PostMeta | null
previous: PostMeta | null
} {
const posts = getAllPostsMeta()
const index = posts.findIndex((post) => post.slug === slug)
const next = index < posts.length - 1 ? posts[index + 1] : null
const previous = index > 0 ? posts[index - 1] : null
return { next, previous }
}
export async function getPostBySlug(slug: string) { export async function getPostBySlug(slug: string) {
const fullPath = path.join(postsDirectory, `${slug}.md`) const fullPath = path.join(postsDirectory, `${slug}.md`)
const { data, content } = matter(fs.readFileSync(fullPath, 'utf8')) const { data, content } = matter(fs.readFileSync(fullPath, 'utf8'))
@ -98,9 +100,12 @@ export async function getPostBySlug(slug: string) {
img: ({ src, alt }: { src: string; alt: string }) => <ImageWrapper src={src} alt={alt} />, img: ({ src, alt }: { src: string; alt: string }) => <ImageWrapper src={src} alt={alt} />,
a: ({ href, children }: { href: string; children: ReactNode }) => a: ({ href, children }: { href: string; children: ReactNode }) =>
href?.startsWith('http') ? ( href?.startsWith('http') ? (
<ExternalLink href={href}>{children}</ExternalLink> <ExternalLink href={href}>
{children}
<ExternalLinkIcon className="inline-block w-3 h-3 ml-1" />
</ExternalLink>
) : ( ) : (
<a href={href}>{children}</a> <Link href={href}>{children}</Link>
), ),
blockquote: ({ children }: { children: ReactNode }) => { blockquote: ({ children }: { children: ReactNode }) => {
const textContent = getTextContent(children).trimStart() const textContent = getTextContent(children).trimStart()