diff --git a/README.md b/README.md
index e215bc4..006aeb3 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,81 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+# Ephemeris
-## Getting Started
+Source of [blog.hangerthem.com](https://blog.hangerthem.com) — my personal blog and portfolio. Posts are plain Markdown files rendered to static HTML at build time by a custom [unified](https://unifiedjs.com/) pipeline.
-First, run the development server:
+## Features
+
+- **Fully static** — every post, tag and category page is pre-rendered with Next.js (App Router, React Server Components)
+- **Custom Markdown extensions** via small rehype plugins in [`lib/`](lib/):
+ - Callouts: `> [!TIP]`, `> [!WARNING]`, `> [!INFO]`, `> [!QUOTE]`
+ - Highlights: `==marked text==`
+ - Definition lists (a term line followed by `: definition` lines)
+ - Code blocks with language label, line numbers and a copy button
+- **Math** with KaTeX and **diagrams** with Mermaid, rendered to SVG at build time — no client-side JS for either
+- **Table of contents** generated from `h2`/`h3` headings with scroll-spy
+- **SEO** — canonical URLs, Open Graph/Twitter cards, generated OG images, JSON-LD, sitemap, robots.txt and an Atom feed
+- Works without JavaScript; JS-only UI (like copy buttons) is hidden when it's unavailable
+
+## Stack
+
+Next.js 16 · React 19 · TypeScript · Tailwind CSS 4 · unified (remark/rehype) · highlight.js · KaTeX · Mermaid (via Puppeteer) · Bun · oxlint/oxfmt
+
+## Getting started
```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
+bun install
+SITE_URL=http://localhost:3000 bun dev
```
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+| Script | Purpose |
+| ------------------ | ------------------------------------ |
+| `bun dev` | Development server |
+| `bun run build` | Production build (static generation) |
+| `bun start` | Serve the production build |
+| `bun run lint` | oxlint + formatting check |
+| `bun run lint:fix` | Auto-fix lint issues and format |
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+### Environment variables
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+| Variable | Required | Description |
+| ---------------- | -------- | ---------------------------------------------------------------- |
+| `SITE_URL` | yes | Public origin, e.g. `https://blog.hangerthem.com` |
+| `SOURCE_COMMIT` | no | Commit hash shown in the sidebar (falls back to `git rev-parse`) |
+| `GIT_ORIGIN_URL` | no | Repository URL; enables the "Source Code" link and commit link |
-## Learn More
+## Writing posts
-To learn more about Next.js, take a look at the following resources:
+Posts live in [`content/posts/`](content/posts/); the file name is the URL slug. Standalone pages (About, Projects, Setup) live in [`content/pages/`](content/pages/).
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+```yaml
+---
+title: 'Tessera: A Privacy-First Card Wallet'
+description: 120–160 characters work best for search results.
+date: 2026-09-18 12:00:00 +0200
+updated: 2026-09-18 12:30:00 +0200 # optional
+category: Projects
+tags: [projects, pwa, privacy]
+coverImage: tessera-cover.png # optional, from public/posts/, 1200×630 recommended
+coverImageAlt: Tessera logo next to two loyalty cards
+math: true # optional, enables KaTeX
+mermaid: true # optional, enables Mermaid diagrams
+draft: true # optional, only rendered in development
+---
+```
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+Frontmatter is validated at build time, so a missing or malformed field fails the build with the file name instead of producing a broken page.
-## Deploy on Vercel
+Images go in [`public/posts/`](public/posts/) and are referenced by file name: ``.
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+## Project structure
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+```
+app/ Routes, metadata, feed, sitemap and OG image generation
+components/ UI split into content/, layout/, post/ and ui/
+content/ Markdown posts and pages
+lib/ Markdown pipeline, rehype plugins, tag/category helpers
+public/posts/ Post images
+```
+
+## Deployment
+
+Deployed with [Nixpacks](https://nixpacks.com/) (see [`nixpacks.toml`](nixpacks.toml)). Mermaid diagrams are rendered with headless Chromium during the build, so the build environment needs Chromium's system libraries and network access to fetch the Mermaid script.
diff --git a/app/(withRecent)/categories/[categorySlug]/page.tsx b/app/(withRecent)/categories/[categorySlug]/page.tsx
index 588a4a6..ca7842d 100644
--- a/app/(withRecent)/categories/[categorySlug]/page.tsx
+++ b/app/(withRecent)/categories/[categorySlug]/page.tsx
@@ -1,8 +1,7 @@
import { pageMetadata } from '@/app/metadata'
import { getAllCategories, getCategoryBySlug, getPostsByCategory } from '@/lib/categories'
-import { dateFormatter } from '@/utils/time'
import type { Metadata } from 'next'
-import Link from 'next/link'
+import { PostList } from '@/components/post/PostList'
import { notFound } from 'next/navigation'
import { slugify } from '@/utils/slug'
@@ -42,30 +41,9 @@ export default async function CategoryPage({ params }: CategoryPageProps) {
return (
-
~/categories/{categorySlug}
+
~/categories/{categorySlug}
-
- {posts.map((post) => (
-
-
-
- {post.title}
-
- {post.date && (
-
- )}
-
-
- ))}
-
+
)
}
diff --git a/app/(withRecent)/categories/page.tsx b/app/(withRecent)/categories/page.tsx
index 1425399..b7f28b5 100644
--- a/app/(withRecent)/categories/page.tsx
+++ b/app/(withRecent)/categories/page.tsx
@@ -3,8 +3,8 @@ import { getAllPostsMeta } from '@/lib/posts'
import { slugify } from '@/utils/slug'
import type { Metadata } from 'next'
import Link from 'next/link'
+import { PostList } from '@/components/post/PostList'
import { pageMetadata } from '@/app/metadata'
-import { dateFormatter } from '@/utils/time'
export async function generateMetadata(): Promise {
return pageMetadata({
@@ -21,7 +21,7 @@ export default function CategoriesPage() {
return (
-
+ )
+}
diff --git a/content/posts/first-post.md b/content/posts/first-post.md
index ffe46b4..02185c1 100644
--- a/content/posts/first-post.md
+++ b/content/posts/first-post.md
@@ -4,8 +4,6 @@ description: Welcome to my blog! I'm Frank Borisjuk, a software developer with a
date: 2024-08-04 15:00:00 +0200
category: Introduction
tags: [introduction, about, tech, books, games]
-math: true
-mermaid: true
---
Hello and welcome to my blog! I'm Frank Borisjuk, and I'm thrilled to have you here on this new journey of sharing my thoughts, projects, and passions with you.
diff --git a/content/posts/self-development.md b/content/posts/self-development.md
index 8a4d536..3caea63 100644
--- a/content/posts/self-development.md
+++ b/content/posts/self-development.md
@@ -4,8 +4,6 @@ description: In this post, I explore the concept of self-development and how gam
date: 2024-08-05 22:00:00 +0200
category: Personal Development
tags: [self-development, gamification, productivity, growth]
-math: true
-mermaid: true
---
> [!QUOTE] "The only person you are destined to become is the person you decide to be."
diff --git a/content/posts/tessera-privacy-first-card-wallet.md b/content/posts/tessera-privacy-first-card-wallet.md
index 3174e8c..50686f5 100644
--- a/content/posts/tessera-privacy-first-card-wallet.md
+++ b/content/posts/tessera-privacy-first-card-wallet.md
@@ -6,7 +6,7 @@ updated: 2026-09-18 12:30:00 +0200
category: Projects
tags: [projects, pwa, privacy, webdev]
coverImage: tessera-cover.png
-coverImageAlt: Tessera cover with basic infroamtion about the post.
+coverImageAlt: Tessera cover with basic information about the post.
---
## The Problem
diff --git a/eslint.config.mjs b/eslint.config.mjs
deleted file mode 100644
index 7fef4d3..0000000
--- a/eslint.config.mjs
+++ /dev/null
@@ -1,18 +0,0 @@
-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,
- ...nextTs,
- // Override default ignores of eslint-config-next.
- globalIgnores([
- // Default ignores of eslint-config-next:
- '.next/**',
- 'out/**',
- 'build/**',
- 'next-env.d.ts',
- ]),
-])
-
-export default eslintConfig
diff --git a/package.json b/package.json
index 9f7654a..62c7748 100644
--- a/package.json
+++ b/package.json
@@ -14,15 +14,11 @@
"dependencies": {
"clsx": "^2.1.1",
"gray-matter": "^4.0.3",
- "hast": "^1.0.0",
"hast-util-from-html-isomorphic": "^2.0.0",
"hast-util-to-text": "^4.0.2",
"highlight.js": "^11.11.1",
"lucide-react": "^1.28.0",
"next": "16.2.12",
- "oxfmt": "^0.61.0",
- "oxlint": "^1.76.0",
- "puppeteer": "^25.4.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"reading-time": "^1.5.0",
@@ -45,8 +41,9 @@
"@types/node": "^26.1.1",
"@types/react": "^19",
"@types/react-dom": "^19",
- "eslint": "^9",
- "eslint-config-next": "16.2.12",
+ "oxfmt": "^0.61.0",
+ "oxlint": "^1.76.0",
+ "puppeteer": "^25.4.0",
"tailwindcss": "^4",
"typescript": "^5"
},
diff --git a/utils/time.ts b/utils/time.ts
index 26f1db4..a4d21e5 100644
--- a/utils/time.ts
+++ b/utils/time.ts
@@ -1,8 +1,25 @@
-/**
- * Utility function to format a date into a human-readable string.
- */
+// Posts are written in Czech time; pin the zone so the build server's timezone doesn't shift dates
+const timeZone = 'Europe/Prague'
+
+/** Long date for listings, e.g. "September 18, 2026". */
export const dateFormatter = new Intl.DateTimeFormat('en', {
day: 'numeric',
month: 'long',
year: 'numeric',
+ timeZone,
+})
+
+/** Compact date for post headers, e.g. "Sep 18, 2026". */
+export const shortDateFormatter = new Intl.DateTimeFormat('en', {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ timeZone,
+})
+
+/** Full date and time, used for hover titles. */
+export const dateTimeFormatter = new Intl.DateTimeFormat('en', {
+ dateStyle: 'long',
+ timeStyle: 'short',
+ timeZone,
})