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",
"endOfLine": "lf",
"insertFinalNewline": true,
"ignorePatterns": [
"node_modules/**",
".cache/**",
"_site/**"
]
}
"ignorePatterns": ["node_modules/**", ".cache/**", "_site/**"]
}

View file

@ -5,7 +5,6 @@ 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 {

View file

@ -11,7 +11,7 @@ export default function BlogIndex() {
}
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>
{posts.length === 0 ? (
<div className="rounded-xl border border-border p-6 text-fg-dim">

View file

@ -1,24 +1,22 @@
import { metadata } from "@/app/metadata"
import { getAllPostsMeta } from "@/lib/posts"
import { metadata } from '@/app/metadata'
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) {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;")
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
export async function GET(req: Request) {
const posts = getAllPostsMeta()
const updated =
posts.length > 0
? new Date(posts[0].date).toISOString()
: new Date().toISOString()
posts.length > 0 ? new Date(posts[0].date).toISOString() : new Date().toISOString()
const entries = posts
.map((p) => {
@ -35,7 +33,7 @@ export async function GET(req: Request) {
<summary>${escapeXml(p.description)}</summary>
</entry>`
})
.join("")
.join('')
const feed = `<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
@ -51,12 +49,12 @@ export async function GET(req: Request) {
${entries}
</feed>`
const accept = req.headers.get("accept") || ""
const contentType = accept.includes("application/atom+xml")
? "application/atom+xml; charset=utf-8"
: "application/xml; charset=utf-8"
const accept = req.headers.get('accept') || ''
const contentType = accept.includes('application/atom+xml')
? 'application/atom+xml; charset=utf-8'
: 'application/xml; charset=utf-8'
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 Script from 'next/script'
import { Sidebar } from '@/components/layout/Sidebar'
import { MobileNavbar } from '@/components/layout/MobileNavbar'
import { metadata } from './metadata'
// eslint-disable-next-line import/no-unassigned-import
import './globals.css'
const spaceGrotesk = Space_Grotesk({
@ -24,6 +26,7 @@ export default function RootLayout({
/>
<body className="min-h-full flex">
<Sidebar />
<MobileNavbar />
{children}
</body>
</html>

View file

@ -1,14 +1,14 @@
import { Metadata } from "next";
import { Metadata } from 'next'
export const metadata: Metadata = {
title: "Ephemeris",
description: "A blog about me and my interests",
creator: "hangerthem",
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",
type: 'website',
title: 'Ephemeris',
description: 'A blog about me and my interests',
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 && (
<Link
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}
</Link>

View file

@ -10,7 +10,7 @@ const noteTypeIcons: Record<NoteType, React.ReactNode> = {
warning: <TriangleAlert className="w-4 h-4" />,
tip: <Lightbulb 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) {

View file

@ -27,7 +27,10 @@ export function CopyButton({ textToCopy, className, ...props }: CopyButtonProps)
return (
<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}
ref={buttonRef}
{...props}

View file

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

View file

@ -1,14 +1,14 @@
import { siBluesky, siX, siFacebook, siReddit } from 'simple-icons';
import { ExternalLink } from '@/components/common/ExternalLink';
import { siBluesky, siX, siFacebook, siReddit } from 'simple-icons'
import { ExternalLink } from '@/components/common/ExternalLink'
type ShareLinksProps = {
url: string;
title: string;
description?: string;
};
url: string
title: string
description?: string
}
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 = [
{
@ -33,7 +33,7 @@ export function ShareLinks({ url, title, description }: ShareLinksProps) {
title,
)}&text=${encodeURIComponent(description ?? '')}`,
},
];
]
return (
<div className="flex gap-4 mx-auto">
@ -46,5 +46,5 @@ export function ShareLinks({ url, title, description }: ShareLinksProps) {
</ExternalLink>
))}
</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'
import { cn } from '@/utils/cn'
import { Folder, GitBranch, Home, Info, Rss, Tags } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { ExternalLink } from '../common/ExternalLink'
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,
},
]
import { navItems } from './navItems'
export function NavLinks() {
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."
>
> - 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.

View file

@ -255,7 +255,6 @@ $$
\label{eq:em_field_strength_tensor}
\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.
2. **Electron Kinetic Term** $(\bar{\psi}(i\gamma^\mu D_\mu-m)\psi)$:
@ -270,7 +269,6 @@ $$
\label{eq:covariant_derivative_for_electrons}
\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.
3. **Electron Mass Term** $(-m)$:
@ -307,7 +305,6 @@ $$
\label{eq:w_field_strength_tensor}
\end{equation}
$$
- **$B$ Field Strength Tensor** ($B_{\mu\nu}$):
$$
@ -316,7 +313,6 @@ $$
\label{eq:b_field_strength_tensor}
\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.
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}
\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.
3. **Lepton/Quark Mass Term** $(-m)$:
@ -366,7 +361,6 @@ $$
\label{eq:gluon_field_strength_tensor}
\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.
2. **Quark Kinetic Term** $(\bar{q}(i\gamma^\mu D_\mu-m_q)q)$:
@ -381,7 +375,6 @@ $$
\label{eq:quark_covariant_derivative_qcd}
\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.
3. **Quark Mass Term** $(-m_q)$:
@ -408,4 +401,4 @@ While the Standard Model is incredibly successful, it has limitations:
- **Gravity**: The Standard Model does not include gravity, which is described by General Relativity and is not quantized.
- **Dark Matter and Dark Energy**: These are components of the universe that do not interact via the Standard Model forces.
- **Neutrino Masses**: Neutrino masses were a later addition to the Standard Model after the discovery of neutrino oscillations.
- **Matter-Antimatter Asymmetry**: The observed dominance of matter over antimatter is not fully explained by the Standard Model.
- **Matter-Antimatter Asymmetry**: The observed dominance of matter over antimatter is not fully explained by the Standard Model.

View file

@ -33,7 +33,7 @@ You can also use other code blocks in your Markdown, like this:
```ts
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.
| Column 1 | Column 2 | Column 3 |
|----------|----------|----------|
| -------- | -------- | -------- |
| Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 |
@ -85,16 +85,15 @@ graph TD;
```
Term 1
: Definition for term 1.
: Definition for term 1.
Term 2
: Definition A for term 2.
: Definition B for term 2, on a new line.
H~2~O
: Definition A for term 2.
: Definition B for term 2, on a new line.
- [ ] Task 1
- [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.
[^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 nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import { defineConfig, globalIgnores } from 'eslint/config'
import nextVitals from 'eslint-config-next/core-web-vitals'
import nextTs from 'eslint-config-next/typescript'
const eslintConfig = defineConfig([
...nextVitals,
@ -8,11 +8,11 @@ const eslintConfig = defineConfig([
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
]),
]);
])
export default eslintConfig;
export default eslintConfig

View file

@ -1,20 +1,20 @@
import { findSlugInArray } from "@/utils/slug"
import { getAllPostsMeta } from "@/lib/posts"
import { findSlugInArray } from '@/utils/slug'
import { getAllPostsMeta } from '@/lib/posts'
/**
* Retrieves all unique categories from the posts and counts the number of posts in each category.
* @returns An object where keys are category names and values are the count of posts in that category.
*/
export function getAllCategories(): Record<string, number> {
const posts = getAllPostsMeta()
const categories = {} as Record<string, number>
const posts = getAllPostsMeta()
const categories = {} as Record<string, number>
posts.forEach((post) => {
const category = post.category
categories[category] = (categories[category] || 0) + 1
})
posts.forEach((post) => {
const category = post.category
categories[category] = (categories[category] || 0) + 1
})
return categories
return categories
}
/** * Finds a category by its slug.
@ -22,8 +22,8 @@ export function getAllCategories(): Record<string, number> {
* @returns The original category name if found, otherwise null.
*/
export function getCategoryBySlug(categorySlug: string): string | null {
const categories = getAllCategories()
return findSlugInArray(categorySlug, Object.keys(categories))
const categories = getAllCategories()
return findSlugInArray(categorySlug, Object.keys(categories))
}
/**
@ -32,12 +32,12 @@ export function getCategoryBySlug(categorySlug: string): string | null {
* @returns An array of posts that belong to the specified category.
*/
export function getPostsByCategory(categorySlug: string) {
const categories = getAllCategories()
const category = findSlugInArray(categorySlug, Object.keys(categories))
const categories = getAllCategories()
const category = findSlugInArray(categorySlug, Object.keys(categories))
if (!category) {
return []
}
if (!category) {
return []
}
return getAllPostsMeta().filter((post) => post.category === category)
}
return getAllPostsMeta().filter((post) => post.category === category)
}

View file

@ -3,10 +3,10 @@ import { Browser, launch } from 'puppeteer'
let browserPromise: Promise<Browser> | null = null
function getBrowser() {
if (!browserPromise) {
browserPromise = launch({ headless: true })
}
return browserPromise
if (!browserPromise) {
browserPromise = launch({ headless: true })
}
return browserPromise
}
/**
@ -14,11 +14,11 @@ function getBrowser() {
* This function is useful for cleaning up resources when the application is shutting down or when the browser is no longer needed.
*/
export async function closeMermaidBrowser() {
if (browserPromise) {
const browser = await browserPromise
await browser.close()
browserPromise = null
}
if (browserPromise) {
const browser = await browserPromise
await browser.close()
browserPromise = null
}
}
const cache = new Map<string, string>()
@ -29,32 +29,32 @@ const cache = new Map<string, string>()
* @returns A promise that resolves to the rendered SVG string.
*/
export async function renderMermaidToSvg(chart: string): Promise<string> {
const cached = cache.get(chart)
if (cached) return cached
const cached = cache.get(chart)
if (cached) return cached
const browser = await getBrowser()
const page = await browser.newPage()
const browser = await getBrowser()
const page = await browser.newPage()
try {
await page.setContent(`
try {
await page.setContent(`
<!DOCTYPE html>
<html><body><div id="target"></div></body></html>
`)
await page.addScriptTag({
url: 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js',
})
await page.addScriptTag({
url: 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js',
})
const svg = await page.evaluate(async (source) => {
// @ts-expect-error mermaid is loaded globally via the script tag above
window.mermaid.initialize({ startOnLoad: false, theme: 'neutral' })
// @ts-expect-error same as above
const { svg: renderedSvg } = await window.mermaid.render('mermaid-diagram', source)
return renderedSvg
}, chart)
const svg = await page.evaluate(async (source) => {
// @ts-expect-error mermaid is loaded globally via the script tag above
window.mermaid.initialize({ startOnLoad: false, theme: 'neutral' })
// @ts-expect-error same as above
const { svg: renderedSvg } = await window.mermaid.render('mermaid-diagram', source)
return renderedSvg
}, chart)
cache.set(chart, svg)
return svg
} finally {
await page.close()
}
}
cache.set(chart, svg)
return svg
} finally {
await page.close()
}
}

View file

@ -3,7 +3,7 @@ import path from 'path'
import matter from 'gray-matter'
import { remark } from 'remark'
import remarkParse from 'remark-parse'
import gfm from 'remark-gfm'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import remarkRehype from 'remark-rehype'
import rehypeHighlight from 'rehype-highlight'
@ -66,7 +66,7 @@ export async function getPostBySlug(slug: string) {
const fullPath = path.join(postsDirectory, `${slug}.md`)
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) {
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.
@ -6,14 +6,14 @@ import { cloneElement, isValidElement, type ReactNode } from "react";
* @returns A string containing the concatenated text content of the ReactNode tree.
*/
export 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 "";
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 ''
}
/**
@ -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.
*/
export function stripLeadingMatch(node: ReactNode, pattern: RegExp): ReactNode {
if (typeof node === "string") {
const trimmed = node.replace(/^\s+/, "");
if (pattern.test(trimmed)) {
return trimmed.replace(pattern, "").replace(/^\s+/, "");
}
return node;
}
if (Array.isArray(node)) {
let stripped = false;
return node.map((child) => {
if (stripped) return child;
if (typeof child === "string" && child.trim() === "") return child; // skip whitespace nodes
if (typeof child === "string") {
const trimmed = child.replace(/^\s+/, "");
if (pattern.test(trimmed)) {
stripped = true;
return trimmed.replace(pattern, "").replace(/^\s+/, "");
}
return child;
}
if (isValidElement(child)) {
stripped = true;
return stripLeadingMatch(child, pattern);
}
return child;
});
}
if (isValidElement(node)) {
const props = node.props as { children?: ReactNode };
return cloneElement(node, {}, stripLeadingMatch(props.children, pattern));
}
return node;
}
if (typeof node === 'string') {
const trimmed = node.replace(/^\s+/, '')
if (pattern.test(trimmed)) {
return trimmed.replace(pattern, '').replace(/^\s+/, '')
}
return node
}
if (Array.isArray(node)) {
let stripped = false
return node.map((child) => {
if (stripped) return child
if (typeof child === 'string' && child.trim() === '') return child // skip whitespace nodes
if (typeof child === 'string') {
const trimmed = child.replace(/^\s+/, '')
if (pattern.test(trimmed)) {
stripped = true
return trimmed.replace(pattern, '').replace(/^\s+/, '')
}
return child
}
if (isValidElement(child)) {
stripped = true
return stripLeadingMatch(child, pattern)
}
return child
})
}
if (isValidElement(node)) {
const props = node.props as { children?: ReactNode }
return cloneElement(node, {}, stripLeadingMatch(props.children, pattern))
}
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 }[] = []
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 })
}
})
@ -25,15 +30,13 @@ export const rehypeMermaidSsg: Plugin<[], Root> = () => async (tree: Root) => {
const svgTree = fromHtmlIsomorphic(svg, { fragment: true })
const svgNode = svgTree.children.find((c) => c.type === 'element') as Element | undefined
if (svgNode) {
; (parent.children as Element[])[index] = svgNode
;(parent.children as Element[])[index] = svgNode
}
} catch (err) {
node.tagName = 'pre'
node.properties = { className: ['text-red-500', 'text-sm'] }
node.children = [
{ type: 'text', value: `Mermaid render error: ${(err as Error).message}` },
]
node.children = [{ 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 = {
/* config options here */
};
images: {
qualities: [75, 100],
},
}
export default nextConfig;
export default nextConfig

View file

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

View file

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

View file

@ -1 +1 @@
export type NoteType = 'warning' | 'tip' | 'info' | 'quote'
export type NoteType = 'warning' | 'tip' | 'info' | 'quote'

View file

@ -1,15 +1,15 @@
export interface PostMeta {
slug: string
title: string
description: string
author: string
date: string
updated?: string
category: string
tags: string[]
math: boolean
mermaid: boolean
readingTimeText: string
coverImage?: string
coverImageAlt?: string
}
slug: string
title: string
description: string
author: string
date: string
updated?: string
category: string
tags: string[]
math: boolean
mermaid: boolean
readingTimeText: string
coverImage?: string
coverImageAlt?: string
}

View file

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

View file

@ -4,11 +4,11 @@
* @returns A URL-friendly slug string.
*/
export function slugify(text: string): string {
return text
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '')
return text
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '')
}
/**
@ -18,10 +18,10 @@ export function slugify(text: string): string {
* @returns The matching string from the array if found, otherwise null.
*/
export function findSlugInArray(slug: string, array: string[]): string | null {
for (const item of array) {
if (slug === slugify(item)) {
return item
}
}
return null
}
for (const item of array) {
if (slug === slugify(item)) {
return item
}
}
return null
}

View file

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