Fixed few formatting things; started on support for mobile/PWA

This commit is contained in:
HangerThem 2026-08-03 15:11:10 +02:00
parent 1a713fe1b6
commit b1de88f998
31 changed files with 276 additions and 239 deletions

View file

@ -10,9 +10,5 @@
"arrowParens": "always", "arrowParens": "always",
"endOfLine": "lf", "endOfLine": "lf",
"insertFinalNewline": true, "insertFinalNewline": true,
"ignorePatterns": [ "ignorePatterns": ["node_modules/**", ".cache/**", "_site/**"]
"node_modules/**",
".cache/**",
"_site/**"
]
} }

View file

@ -5,7 +5,6 @@ import type { Metadata } from 'next'
import Link from 'next/link' import Link from 'next/link'
import { metadata } from '@/app/metadata' import { metadata } from '@/app/metadata'
import { dateFormatter } from '@/utils/time' import { dateFormatter } from '@/utils/time'
import { RecentlyUpdatedPosts } from '@/components/common/RecentlyUpdatedPosts'
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
return { return {

View file

@ -11,7 +11,7 @@ export default function BlogIndex() {
} }
return ( return (
<main className="max-w-3xl mx-auto px-4 py-8 lg:py-16"> <main className="max-w-3xl mx-auto px-4 py-8 mb-16 lg:py-16">
<section> <section>
{posts.length === 0 ? ( {posts.length === 0 ? (
<div className="rounded-xl border border-border p-6 text-fg-dim"> <div className="rounded-xl border border-border p-6 text-fg-dim">

View file

@ -1,24 +1,22 @@
import { metadata } from "@/app/metadata" import { metadata } from '@/app/metadata'
import { getAllPostsMeta } from "@/lib/posts" import { getAllPostsMeta } from '@/lib/posts'
const SITE_URL = process.env.SITE_URL || "http://localhost:3000" const SITE_URL = process.env.SITE_URL || 'http://localhost:3000'
function escapeXml(str: string) { function escapeXml(str: string) {
return str return str
.replace(/&/g, "&amp;") .replace(/&/g, '&amp;')
.replace(/</g, "&lt;") .replace(/</g, '&lt;')
.replace(/>/g, "&gt;") .replace(/>/g, '&gt;')
.replace(/"/g, "&quot;") .replace(/"/g, '&quot;')
.replace(/'/g, "&apos;") .replace(/'/g, '&apos;')
} }
export async function GET(req: Request) { export async function GET(req: Request) {
const posts = getAllPostsMeta() const posts = getAllPostsMeta()
const updated = const updated =
posts.length > 0 posts.length > 0 ? new Date(posts[0].date).toISOString() : new Date().toISOString()
? new Date(posts[0].date).toISOString()
: new Date().toISOString()
const entries = posts const entries = posts
.map((p) => { .map((p) => {
@ -35,7 +33,7 @@ export async function GET(req: Request) {
<summary>${escapeXml(p.description)}</summary> <summary>${escapeXml(p.description)}</summary>
</entry>` </entry>`
}) })
.join("") .join('')
const feed = `<?xml version="1.0" encoding="UTF-8"?> const feed = `<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"> <feed xmlns="http://www.w3.org/2005/Atom">
@ -51,12 +49,12 @@ export async function GET(req: Request) {
${entries} ${entries}
</feed>` </feed>`
const accept = req.headers.get("accept") || "" const accept = req.headers.get('accept') || ''
const contentType = accept.includes("application/atom+xml") const contentType = accept.includes('application/atom+xml')
? "application/atom+xml; charset=utf-8" ? 'application/atom+xml; charset=utf-8'
: "application/xml; charset=utf-8" : 'application/xml; charset=utf-8'
return new Response(feed, { return new Response(feed, {
headers: { "Content-Type": contentType }, headers: { 'Content-Type': contentType },
}) })
} }

View file

@ -1,7 +1,9 @@
import { Space_Grotesk } from 'next/font/google' import { Space_Grotesk } from 'next/font/google'
import Script from 'next/script' import Script from 'next/script'
import { Sidebar } from '@/components/layout/Sidebar' import { Sidebar } from '@/components/layout/Sidebar'
import { MobileNavbar } from '@/components/layout/MobileNavbar'
import { metadata } from './metadata' import { metadata } from './metadata'
// eslint-disable-next-line import/no-unassigned-import
import './globals.css' import './globals.css'
const spaceGrotesk = Space_Grotesk({ const spaceGrotesk = Space_Grotesk({
@ -24,6 +26,7 @@ export default function RootLayout({
/> />
<body className="min-h-full flex"> <body className="min-h-full flex">
<Sidebar /> <Sidebar />
<MobileNavbar />
{children} {children}
</body> </body>
</html> </html>

View file

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

View file

@ -47,7 +47,7 @@ export default async function Post({ params }: { params: Promise<{ slug: string
{meta.category && ( {meta.category && (
<Link <Link
href={`/categories/${slugify(meta.category)}`} href={`/categories/${slugify(meta.category)}`}
className="text-accent hover:underline font-sm mb-2 block" className="text-accent hover:underline font-sm mb-2 inline-block"
> >
{meta.category} {meta.category}
</Link> </Link>

View file

@ -10,7 +10,7 @@ const noteTypeIcons: Record<NoteType, React.ReactNode> = {
warning: <TriangleAlert className="w-4 h-4" />, warning: <TriangleAlert className="w-4 h-4" />,
tip: <Lightbulb className="w-4 h-4" />, tip: <Lightbulb className="w-4 h-4" />,
info: <FileWarning className="w-4 h-4" />, info: <FileWarning className="w-4 h-4" />,
quote: <Quote className="w-4 h-4" /> quote: <Quote className="w-4 h-4" />,
} }
export function BlockquoteWrapper({ type, children }: BlockquoteWrapperProps) { export function BlockquoteWrapper({ type, children }: BlockquoteWrapperProps) {

View file

@ -27,7 +27,10 @@ export function CopyButton({ textToCopy, className, ...props }: CopyButtonProps)
return ( return (
<button <button
className={cn('text-fg-dim hover:text-fg transition-colors js-only text-xs cursor-pointer disabled:cursor-default disabled:hover:text-fg-dim', className)} className={cn(
'text-fg-dim hover:text-fg transition-colors js-only text-xs cursor-pointer disabled:cursor-default disabled:hover:text-fg-dim',
className,
)}
onClick={handleCopy} onClick={handleCopy}
ref={buttonRef} ref={buttonRef}
{...props} {...props}

View file

@ -27,7 +27,7 @@ export function ImageWrapper({ src, alt, isListing }: ImageWrapperProps) {
alt={alt ?? ''} alt={alt ?? ''}
width={isListing ? 400 : 800} width={isListing ? 400 : 800}
height={isListing ? 240 : 400} height={isListing ? 240 : 400}
quality={100} quality={isListing ? 75 : 100}
priority={true} priority={true}
className="w-auto mx-auto object-cover h-full" className="w-auto mx-auto object-cover h-full"
/> />

View file

@ -1,14 +1,14 @@
import { siBluesky, siX, siFacebook, siReddit } from 'simple-icons'; import { siBluesky, siX, siFacebook, siReddit } from 'simple-icons'
import { ExternalLink } from '@/components/common/ExternalLink'; import { ExternalLink } from '@/components/common/ExternalLink'
type ShareLinksProps = { type ShareLinksProps = {
url: string; url: string
title: string; title: string
description?: string; description?: string
}; }
export function ShareLinks({ url, title, description }: ShareLinksProps) { export function ShareLinks({ url, title, description }: ShareLinksProps) {
const shareText = `Check out this post: ${title} - ${description}\n\n${url}`; const shareText = `Check out this post: ${title} - ${description}\n\n${url}`
const networks = [ const networks = [
{ {
@ -33,7 +33,7 @@ export function ShareLinks({ url, title, description }: ShareLinksProps) {
title, title,
)}&text=${encodeURIComponent(description ?? '')}`, )}&text=${encodeURIComponent(description ?? '')}`,
}, },
]; ]
return ( return (
<div className="flex gap-4 mx-auto"> <div className="flex gap-4 mx-auto">
@ -46,5 +46,5 @@ export function ShareLinks({ url, title, description }: ShareLinksProps) {
</ExternalLink> </ExternalLink>
))} ))}
</div> </div>
); )
} }

View file

@ -0,0 +1,38 @@
'use client'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
import { navItems } from './navItems'
import { cn } from '@/utils/cn'
export function MobileNavbar() {
const pathname = usePathname()
const isActive = (href: string) => {
if (href === '/') {
return pathname === href
}
return pathname?.startsWith(href)
}
return (
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-bg border-t border-border z-50">
<div className="flex justify-between items-center p-4 max-w-90 mx-auto">
{navItems
.filter((item) => !item.isExternal)
.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
'transition-colors',
isActive(item.href) ? 'text-accent' : 'text-fg-dim hover:text-fg',
)}
>
{item.icon}
</Link>
))}
</div>
</nav>
)
}

View file

@ -1,36 +1,10 @@
'use client' 'use client'
import { cn } from '@/utils/cn' import { cn } from '@/utils/cn'
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' import { ExternalLink } from '../common/ExternalLink'
import { navItems } from './navItems'
type NavItem = {
href: string
label: string
icon: React.ReactNode
isExternal?: boolean
}
const navItems: NavItem[] = [
{ href: '/', label: 'Home', icon: <Home className="w-4 h-4" /> },
{ href: '/categories', label: 'Categories', icon: <Folder className="w-4 h-4" /> },
{ href: '/about', label: 'About', icon: <Info className="w-4 h-4" /> },
{ href: '/tags', label: 'Tags', icon: <Tags className="w-4 h-4" /> },
{
href: '/feed.xml',
label: 'RSS Feed',
icon: <Rss className="w-4 h-4" />,
isExternal: true,
},
{
href: 'https://git.hangerthem.com/hangerthem/blog.hangerthem.com',
label: 'Source Code',
icon: <GitBranch className="w-4 h-4" />,
isExternal: true,
},
]
export function NavLinks() { export function NavLinks() {
const pathname = usePathname() const pathname = usePathname()

View file

@ -0,0 +1,29 @@
import { Folder, GitBranch, Home, Info, Rss, Tags } from 'lucide-react'
type NavItem = {
href: string
label: string
icon: React.ReactNode
isExternal?: boolean
}
const navIconsClassName = 'w-6 h-6'
export const navItems: NavItem[] = [
{ href: '/', label: 'Home', icon: <Home /> },
{ href: '/about', label: 'About', icon: <Info className={navIconsClassName} /> },
{ href: '/categories', label: 'Categories', icon: <Folder className={navIconsClassName} /> },
{ href: '/tags', label: 'Tags', icon: <Tags className={navIconsClassName} /> },
{
href: '/feed.xml',
label: 'RSS Feed',
icon: <Rss className={navIconsClassName} />,
isExternal: true,
},
{
href: 'https://git.hangerthem.com/hangerthem/blog.hangerthem.com',
label: 'Source Code',
icon: <GitBranch className={navIconsClassName} />,
isExternal: true,
},
]

View file

@ -10,6 +10,7 @@ mermaid: true
--- ---
> [!QUOTE] "The only person you are destined to become is the person you decide to be." > [!QUOTE] "The only person you are destined to become is the person you decide to be."
>
> - Ralph Waldo Emerson > - Ralph Waldo Emerson
Right at the beginning of this post, I want to make it clear that I am not a self-help guru or a life coach. I am just a regular person who is passionate about personal development and enjoys exploring ways to improve myself. In this post, I want to share some thoughts on self-development and how gamification can be used to enhance productivity and personal growth. Right at the beginning of this post, I want to make it clear that I am not a self-help guru or a life coach. I am just a regular person who is passionate about personal development and enjoys exploring ways to improve myself. In this post, I want to share some thoughts on self-development and how gamification can be used to enhance productivity and personal growth.

View file

@ -255,7 +255,6 @@ $$
\label{eq:em_field_strength_tensor} \label{eq:em_field_strength_tensor}
\end{equation} \end{equation}
$$ $$
- **Explanation**: This term describes the field strength of the electromagnetic field. It involves the derivative of the electromagnetic potential $A_\mu$. The field strength tensor is antisymmetric, reflecting the nature of the electromagnetic force. - **Explanation**: This term describes the field strength of the electromagnetic field. It involves the derivative of the electromagnetic potential $A_\mu$. The field strength tensor is antisymmetric, reflecting the nature of the electromagnetic force.
2. **Electron Kinetic Term** $(\bar{\psi}(i\gamma^\mu D_\mu-m)\psi)$: 2. **Electron Kinetic Term** $(\bar{\psi}(i\gamma^\mu D_\mu-m)\psi)$:
@ -270,7 +269,6 @@ $$
\label{eq:covariant_derivative_for_electrons} \label{eq:covariant_derivative_for_electrons}
\end{equation} \end{equation}
$$ $$
- **Explanation**: This accounts for the interaction of electrons with the electromagnetic field. $e$ is the electric charge, and $A_\mu$ is the electromagnetic potential. - **Explanation**: This accounts for the interaction of electrons with the electromagnetic field. $e$ is the electric charge, and $A_\mu$ is the electromagnetic potential.
3. **Electron Mass Term** $(-m)$: 3. **Electron Mass Term** $(-m)$:
@ -307,7 +305,6 @@ $$
\label{eq:w_field_strength_tensor} \label{eq:w_field_strength_tensor}
\end{equation} \end{equation}
$$ $$
- **$B$ Field Strength Tensor** ($B_{\mu\nu}$): - **$B$ Field Strength Tensor** ($B_{\mu\nu}$):
$$ $$
@ -316,7 +313,6 @@ $$
\label{eq:b_field_strength_tensor} \label{eq:b_field_strength_tensor}
\end{equation} \end{equation}
$$ $$
- **Explanation**: These terms describe the field strengths of the $W$ and $B$ fields, which are associated with the weak and electromagnetic forces, respectively. The $W$ field strength tensor involves the non-Abelian field strength term, reflecting the non-Abelian nature of the weak force. - **Explanation**: These terms describe the field strengths of the $W$ and $B$ fields, which are associated with the weak and electromagnetic forces, respectively. The $W$ field strength tensor involves the non-Abelian field strength term, reflecting the non-Abelian nature of the weak force.
2. **Lepton/Quark Kinetic Term** $(\bar{\psi}(i\gamma^\mu D_\mu-m)\psi)$: 2. **Lepton/Quark Kinetic Term** $(\bar{\psi}(i\gamma^\mu D_\mu-m)\psi)$:
@ -331,7 +327,6 @@ $$
\label{eq:covariant_derivative_for_leptons_quarks} \label{eq:covariant_derivative_for_leptons_quarks}
\end{equation} \end{equation}
$$ $$
- **Explanation**: This accounts for the interaction of leptons/quarks with the $W$ and $B$ fields. $g'$ and $g$ are the coupling constants for the $U(1)$ and $SU(2)$ gauge groups, respectively. $Y$ is the weak hypercharge, and $\frac{\tau^a}{2}$ are the generators of the $SU(2)$ group. - **Explanation**: This accounts for the interaction of leptons/quarks with the $W$ and $B$ fields. $g'$ and $g$ are the coupling constants for the $U(1)$ and $SU(2)$ gauge groups, respectively. $Y$ is the weak hypercharge, and $\frac{\tau^a}{2}$ are the generators of the $SU(2)$ group.
3. **Lepton/Quark Mass Term** $(-m)$: 3. **Lepton/Quark Mass Term** $(-m)$:
@ -366,7 +361,6 @@ $$
\label{eq:gluon_field_strength_tensor} \label{eq:gluon_field_strength_tensor}
\end{equation} \end{equation}
$$ $$
- **Explanation**: This term describes the field strength of gluons. It involves the derivative of the gluon fields $(\partial_\mu G^a_\nu$ and $\partial_\nu G^a_\mu)$ and the non-Abelian field strength term involving the structure constants $f^{abc}$, which accounts for the interaction between gluons themselves. - **Explanation**: This term describes the field strength of gluons. It involves the derivative of the gluon fields $(\partial_\mu G^a_\nu$ and $\partial_\nu G^a_\mu)$ and the non-Abelian field strength term involving the structure constants $f^{abc}$, which accounts for the interaction between gluons themselves.
2. **Quark Kinetic Term** $(\bar{q}(i\gamma^\mu D_\mu-m_q)q)$: 2. **Quark Kinetic Term** $(\bar{q}(i\gamma^\mu D_\mu-m_q)q)$:
@ -381,7 +375,6 @@ $$
\label{eq:quark_covariant_derivative_qcd} \label{eq:quark_covariant_derivative_qcd}
\end{equation} \end{equation}
$$ $$
- **Explanation**: This accounts for the interaction of quarks with gluons. $g_s$ is the strong coupling constant, and $\frac{\lambda^a}{2}$ are the generators of the SU(3) color gauge group. $G^a_\mu$ are the gluon fields. - **Explanation**: This accounts for the interaction of quarks with gluons. $g_s$ is the strong coupling constant, and $\frac{\lambda^a}{2}$ are the generators of the SU(3) color gauge group. $G^a_\mu$ are the gluon fields.
3. **Quark Mass Term** $(-m_q)$: 3. **Quark Mass Term** $(-m_q)$:

View file

@ -33,7 +33,7 @@ You can also use other code blocks in your Markdown, like this:
```ts ```ts
function helloWorld() { function helloWorld() {
console.log("Hello, world!"); console.log('Hello, world!')
} }
``` ```
@ -54,7 +54,7 @@ It will be rendered as:
> [!TIP] You can also use `>` to add blockquotes to your posts. > [!TIP] You can also use `>` to add blockquotes to your posts.
| Column 1 | Column 2 | Column 3 | | Column 1 | Column 2 | Column 3 |
|----------|----------|----------| | -------- | -------- | -------- |
| Cell 1 | Cell 2 | Cell 3 | | Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 | | Cell 4 | Cell 5 | Cell 6 |
@ -91,10 +91,9 @@ Term 2
: Definition A for term 2. : Definition A for term 2.
: Definition B for term 2, on a new line. : Definition B for term 2, on a new line.
H~2~O
- [ ] Task 1 - [ ] Task 1
- [x] Task 2 - [x] Task 2
[^1]: The `src/assets/blog` ==directory== is where you can place images for your blog posts. You can reference them in your Markdown using the relative path to the image file. [^1]: The `src/assets/blog` ==directory== is where you can place images for your blog posts. You can reference them in your Markdown using the relative path to the image file.
[^2]: You can also add footnotes to your posts. This is a footnote reference, and the footnote text is at the bottom of the post. [^2]: You can also add footnotes to your posts. This is a footnote reference, and the footnote text is at the bottom of the post.

View file

@ -1,6 +1,6 @@
import { defineConfig, globalIgnores } from "eslint/config"; import { defineConfig, globalIgnores } from 'eslint/config'
import nextVitals from "eslint-config-next/core-web-vitals"; import nextVitals from 'eslint-config-next/core-web-vitals'
import nextTs from "eslint-config-next/typescript"; import nextTs from 'eslint-config-next/typescript'
const eslintConfig = defineConfig([ const eslintConfig = defineConfig([
...nextVitals, ...nextVitals,
@ -8,11 +8,11 @@ const eslintConfig = defineConfig([
// Override default ignores of eslint-config-next. // Override default ignores of eslint-config-next.
globalIgnores([ globalIgnores([
// Default ignores of eslint-config-next: // Default ignores of eslint-config-next:
".next/**", '.next/**',
"out/**", 'out/**',
"build/**", 'build/**',
"next-env.d.ts", 'next-env.d.ts',
]), ]),
]); ])
export default eslintConfig; export default eslintConfig

View file

@ -1,5 +1,5 @@
import { findSlugInArray } from "@/utils/slug" import { findSlugInArray } from '@/utils/slug'
import { getAllPostsMeta } from "@/lib/posts" import { getAllPostsMeta } from '@/lib/posts'
/** /**
* Retrieves all unique categories from the posts and counts the number of posts in each category. * Retrieves all unique categories from the posts and counts the number of posts in each category.

View file

@ -3,7 +3,7 @@ import path from 'path'
import matter from 'gray-matter' import matter from 'gray-matter'
import { remark } from 'remark' import { remark } from 'remark'
import remarkParse from 'remark-parse' import remarkParse from 'remark-parse'
import gfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math' import remarkMath from 'remark-math'
import remarkRehype from 'remark-rehype' import remarkRehype from 'remark-rehype'
import rehypeHighlight from 'rehype-highlight' import rehypeHighlight from 'rehype-highlight'
@ -66,7 +66,7 @@ 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'))
let processor = remark().use(remarkParse).use(gfm) let processor = remark().use(remarkParse).use(remarkGfm)
if (data.math) { if (data.math) {
processor = processor.use(remarkMath) processor = processor.use(remarkMath)

View file

@ -1,4 +1,4 @@
import { cloneElement, isValidElement, type ReactNode } from "react"; import { cloneElement, isValidElement, type ReactNode } from 'react'
/** /**
* Recursively traverses a ReactNode tree and extracts the text content. * Recursively traverses a ReactNode tree and extracts the text content.
@ -6,14 +6,14 @@ import { cloneElement, isValidElement, type ReactNode } from "react";
* @returns A string containing the concatenated text content of the ReactNode tree. * @returns A string containing the concatenated text content of the ReactNode tree.
*/ */
export function getTextContent(node: ReactNode): string { export function getTextContent(node: ReactNode): string {
if (node === null || typeof node === "boolean") return ""; if (node === null || typeof node === 'boolean') return ''
if (typeof node === "string" || typeof node === "number") return String(node); if (typeof node === 'string' || typeof node === 'number') return String(node)
if (Array.isArray(node)) return node.map(getTextContent).join(""); if (Array.isArray(node)) return node.map(getTextContent).join('')
if (isValidElement(node)) { if (isValidElement(node)) {
const props = node.props as { children?: ReactNode }; const props = node.props as { children?: ReactNode }
return getTextContent(props.children); return getTextContent(props.children)
} }
return ""; return ''
} }
/** /**
@ -23,36 +23,36 @@ export function getTextContent(node: ReactNode): string {
* @returns A new ReactNode with the leading whitespace and specified pattern removed from text nodes. * @returns A new ReactNode with the leading whitespace and specified pattern removed from text nodes.
*/ */
export function stripLeadingMatch(node: ReactNode, pattern: RegExp): ReactNode { export function stripLeadingMatch(node: ReactNode, pattern: RegExp): ReactNode {
if (typeof node === "string") { if (typeof node === 'string') {
const trimmed = node.replace(/^\s+/, ""); const trimmed = node.replace(/^\s+/, '')
if (pattern.test(trimmed)) { if (pattern.test(trimmed)) {
return trimmed.replace(pattern, "").replace(/^\s+/, ""); return trimmed.replace(pattern, '').replace(/^\s+/, '')
} }
return node; return node
} }
if (Array.isArray(node)) { if (Array.isArray(node)) {
let stripped = false; let stripped = false
return node.map((child) => { return node.map((child) => {
if (stripped) return child; if (stripped) return child
if (typeof child === "string" && child.trim() === "") return child; // skip whitespace nodes if (typeof child === 'string' && child.trim() === '') return child // skip whitespace nodes
if (typeof child === "string") { if (typeof child === 'string') {
const trimmed = child.replace(/^\s+/, ""); const trimmed = child.replace(/^\s+/, '')
if (pattern.test(trimmed)) { if (pattern.test(trimmed)) {
stripped = true; stripped = true
return trimmed.replace(pattern, "").replace(/^\s+/, ""); return trimmed.replace(pattern, '').replace(/^\s+/, '')
} }
return child; return child
} }
if (isValidElement(child)) { if (isValidElement(child)) {
stripped = true; stripped = true
return stripLeadingMatch(child, pattern); return stripLeadingMatch(child, pattern)
} }
return child; return child
}); })
} }
if (isValidElement(node)) { if (isValidElement(node)) {
const props = node.props as { children?: ReactNode }; const props = node.props as { children?: ReactNode }
return cloneElement(node, {}, stripLeadingMatch(props.children, pattern)); return cloneElement(node, {}, stripLeadingMatch(props.children, pattern))
} }
return node; return node
} }

View file

@ -12,7 +12,12 @@ export const rehypeMermaidSsg: Plugin<[], Root> = () => async (tree: Root) => {
const targets: { node: Element; parent: Element | Root; index: number }[] = [] const targets: { node: Element; parent: Element | Root; index: number }[] = []
visit(tree, 'element', (node: Element, index, parent) => { visit(tree, 'element', (node: Element, index, parent) => {
if (node.tagName === 'div' && node.properties?.['data-mermaid'] && parent && typeof index === 'number') { if (
node.tagName === 'div' &&
node.properties?.['data-mermaid'] &&
parent &&
typeof index === 'number'
) {
targets.push({ node, parent: parent as Element | Root, index }) targets.push({ node, parent: parent as Element | Root, index })
} }
}) })
@ -30,9 +35,7 @@ export const rehypeMermaidSsg: Plugin<[], Root> = () => async (tree: Root) => {
} catch (err) { } catch (err) {
node.tagName = 'pre' node.tagName = 'pre'
node.properties = { className: ['text-red-500', 'text-sm'] } node.properties = { className: ['text-red-500', 'text-sm'] }
node.children = [ node.children = [{ type: 'text', value: `Mermaid render error: ${(err as Error).message}` }]
{ type: 'text', value: `Mermaid render error: ${(err as Error).message}` },
]
} }
}), }),
) )

View file

@ -1,7 +1,9 @@
import type { NextConfig } from "next"; import type { NextConfig } from 'next'
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ images: {
}; qualities: [75, 100],
},
}
export default nextConfig; export default nextConfig

View file

@ -9,8 +9,7 @@
"lint": "oxlint . && oxfmt --check", "lint": "oxlint . && oxfmt --check",
"lint:fix": "oxlint . --fix && oxfmt --write", "lint:fix": "oxlint . --fix && oxfmt --write",
"format": "oxfmt --check", "format": "oxfmt --check",
"format:write": "oxfmt --write", "format:write": "oxfmt --write"
"postinstall": "puppeteer browsers install chrome"
}, },
"dependencies": { "dependencies": {
"clsx": "^2.1.1", "clsx": "^2.1.1",

View file

@ -1,7 +1,7 @@
const config = { const config = {
plugins: { plugins: {
"@tailwindcss/postcss": {}, '@tailwindcss/postcss': {},
}, },
}; }
export default config; export default config

View file

@ -1,10 +1,10 @@
import { clsx, type ClassValue } from 'clsx'; import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge'
/** /**
* Utility function to merge Tailwind CSS classes * Utility function to merge Tailwind CSS classes
* Combines clsx for conditional classes with tailwind-merge for deduplication * Combines clsx for conditional classes with tailwind-merge for deduplication
*/ */
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs))
} }

View file

@ -5,4 +5,4 @@ export const dateFormatter = new Intl.DateTimeFormat('en', {
day: 'numeric', day: 'numeric',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
}); })