Fixed few SSG issues and cleanedup about me

This commit is contained in:
HangerThem 2026-08-04 08:52:43 +02:00
parent ff553187db
commit 1a5ea2f6c0
4 changed files with 69 additions and 78 deletions

View file

@ -1,7 +1,7 @@
import { getAllPostsMeta } from '@/lib/posts'
import { Post } from '@/components/post/Post'
export default function BlogIndex() {
export default function Homepage() {
const posts = getAllPostsMeta()
const rest = posts.slice(1)

View file

@ -1,17 +1,7 @@
import { metadata } from '@/app/metadata'
import { ImageWrapper } from '@/components/content/ImageWrapper'
import { TableOfContents } from '@/components/post/TableOfContents'
import { getPostBySlug } from '@/lib/posts'
import { slugify } from '@/utils/slug'
import { Pencil, SendHorizonal } from 'lucide-react'
import type { Metadata } from 'next'
import Link from 'next/link'
export async function generateStaticParams() {
return await getPostBySlug('about', 'content/pages')
}
export const dynamicParams = false
export async function generateMetadata(): Promise<Metadata> {
const { meta } = await getPostBySlug('about', 'content/pages')
@ -29,7 +19,7 @@ export async function generateMetadata(): Promise<Metadata> {
}
export default async function Post() {
const { meta, content } = await getPostBySlug('about', 'content/pages')
const { meta, content, tableOfContents } = await getPostBySlug('about', 'content/pages')
return (
<main className="flex w-full">
@ -39,7 +29,7 @@ export default async function Post() {
<div className="prose">{content}</div>
</article>
<div className="w-80 2xl:w-100 hidden lg:block" />
<TableOfContents items={tableOfContents} />
</main>
)
}

View file

@ -1,16 +1,41 @@
---
title: About
description: Learn more about me, my background, and my passions. Discover what drives me as a software developer, IT instructor, and lifelong learner. Get to know the person behind the code and the projects.
description: Learn more about me, my background, and my passions. Discover what drives me as a software developer, IT instructor, and lifelong learner.
---
Hey, I'm Frank (František Borisjuk) — a freelance full-stack developer and IT instructor based in Czechia.
Hi there! I'm Frank Borisjuk, a software developer from the north of Czechia. At 21, I've spent the last few years diving deep into code and technology, driven by a stubborn need to learn how things work and then build them myself.
By day, I build things with Next.js, TypeScript, Supabase, and Prisma — mostly full-stack web projects for clients, ranging from B2B platforms to niche tools that solve one very specific problem well. I also run my own self-hosted infrastructure stack (Coolify, Traefik, Authentik, Forgejo, and the rest) on a Hetzner VPS, because I like understanding — and controlling — every layer of what I ship.
## What I Do
I also teach. At my old high school, I run a web development and cybersecurity curriculum for teenagers, which somehow keeps me both humble and sharp — nothing forces you to actually understand something like explaining it to a room of sixteen-year-olds.
By day (and often night), I'm a freelance full-stack developer working mostly in Next.js, TypeScript, Supabase, and Prisma. My skills also extend to Python, C# and some basics of C, Go and Rust, and whatever else a project calls for — I like exploring new technologies more than sticking to one stack out of habit.
Outside of client work, I tinker on my own projects: an open-source cookie consent library built on an anti-dark-pattern philosophy, a branching narrative RPG engine, and the occasional deep dive into things that have nothing to do with code — Czech constitutional law, obscure mythology, grimdark fiction, Linux package conflicts at 2am.
I also teach web development and cybersecurity to teenagers at my old high school, which keeps me sharper than any tutorial ever could. Nothing exposes a shaky understanding like trying to explain it clearly.
I run Arch (btw), care a lot about privacy and self-hosting, and generally believe most software problems are actually just under-specified problems in disguise.
Some things I've built or am building: [linkboard](https://github.com/HangerThem/linkboard) a linktree alternative — currently being rewritten; [lrc-audio-player](https://github.com/HangerThem/lrc-audio-player), a small but effective package for playing audio with synchronized lyrics; and [this blog](/) itself. There are a few other open-source projects scattered across my [GitHub](https://github.com/HangerThem), if you feel like digging.
This site is where I write about what I'm building, what I'm learning, and occasionally what's breaking. Feel free to look around, or reach out at [f.borisjuk@hangerthem.com](mailto:f.borisjuk@hangerthem.com).
I also run my own self-hosted infrastructure on a Hetzner VPS, because I like understanding every layer of what I ship, not just the parts a framework hides from me.
### Groups
I'm active in [gly.cz](https://gly.cz), a webring community I co-founded with my friend [Filip](https://blog.trnck.dev).
## My Passions
- **Reading** — I have a deep appreciation for anything with a compelling story and excellent writing. Tolkien remains a favorite, and I've been pulled into deep dives on mythology, theology, and obscure traditions ever since.
- **Video Games** — I lean toward rich narratives, like *Hollow Knight*, alongside relaxing builders like *Minecraft*, *Stardew Valley*, and *Terraria*. Not much of a BR or FPS person.
- **TTRPG** — A forever Dungeon Master at heart. World-building, storytelling, and the shared creative chaos of a good session are hard to beat. I'm not actively running a table right now, but I still enjoy reading new systems and stealing ideas for worlds I haven't built yet.
- **Programming** — Web development is my main craft, but I like poking at whatever's unfamiliar. Lately that's meant self-hosting, privacy tooling, and running my own Linux infrastructure end to end.
## Why This Blog?
I started this blog to share my journey, insights, and passions with others — whether that's technology, books, gaming, or the odd rabbit hole I've fallen into. Expect a mix of personal reflections and useful, practical information. Mostly, I hope you enjoy reading it as much as I enjoy writing it.
## A Bit More About Me
My path into programming started with a love of learning and problem-solving, and that hasn't changed. In D&D, the joy comes from shared experience with friends and the thrill of building new worlds. I try to keep my days simple: a loose plan for what needs doing, then following my interests as they come.
Living in Czechia offers a solid blend of modern comfort and rich culture — a manageable size, good education and healthcare, and, of course, excellent beer.
---
Feel free to look around, or reach out at [f.borisjuk@hangerthem.com](mailto:f.borisjuk@hangerthem.com) or on [GitHub](https://github.com/HangerThem).

View file

@ -3,26 +3,26 @@ import type { Plugin } from 'unified'
import { visit } from 'unist-util-visit'
/**
* Strips a leading match from the first text node in the given children array.
* @param children The children array to process.
* @param regex The regex pattern to match and strip from the first text node.
* @returns The modified children array with the leading match stripped.
* Checks whether a line starts with a colon marker (optionally preceded by
* whitespace) and, if so, strips it and returns the remaining content.
* @param line The line (array of ElementContent) to check.
* @returns Whether the line is a colon-prefixed definition line, and its content with the marker stripped.
*/
const stripLeadingColon = (
line: ElementContent[],
): { isNewDefinition: boolean; content: ElementContent[] } => {
): { isDefinitionLine: boolean; content: ElementContent[] } => {
const [first, ...rest] = line
if (first?.type === 'text') {
const match = first.value.match(/^[ \t]*:[ \t]*/)
if (match) {
const remainder = first.value.slice(match[0].length)
return {
isNewDefinition: true,
isDefinitionLine: true,
content: remainder ? [{ type: 'text', value: remainder } as Text, ...rest] : rest,
}
}
}
return { isNewDefinition: false, content: line }
return { isDefinitionLine: false, content: line }
}
/**
@ -53,69 +53,45 @@ const splitIntoLines = (nodes: ElementContent[]): ElementContent[][] => {
}
/**
* A rehype plugin that transforms paragraphs containing a leading "Term:" into
* definition lists. For example, a paragraph like "Term: Definition" will be
* transformed into a <dl> element with a <dt> for the term and a <dd> for the
* definition.
* A rehype plugin that transforms paragraphs shaped like:
*
* Term
* : Definition A for term.
* : Definition B for term, on a new line.
*
* into a <dl> element with a <dt> for the term and a <dd> per colon-prefixed
* line. The term must sit on its own line, and every subsequent non-empty
* line must start with ":" otherwise the paragraph is left untouched, so
* ordinary sentences containing a colon (e.g. "My passions: A, B and C")
* are never affected.
*/
export const rehypeDefinition: Plugin<[], Root> = () => (tree: Root) => {
visit(tree, 'element', (node: Element, index, parent) => {
if (!parent || typeof index !== 'number') return
if (node.tagName !== 'p') return
const children = node.children
const lines = splitIntoLines(node.children)
let splitIndex = -1
let colonIndex = -1
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child.type === 'text') {
const idx = child.value.indexOf(':')
if (idx !== -1) {
splitIndex = i
colonIndex = idx
break
}
}
}
// Need at least a term line and one definition line.
if (lines.length < 2) return
if (splitIndex === -1) return
const [termLine, ...restLines] = lines
const splitText = children[splitIndex] as Text
const beforeColon = splitText.value.slice(0, colonIndex)
const afterColon = splitText.value.slice(colonIndex + 1).replace(/^[ \t]+/, '')
if (termLine.length === 0) return
// The term line itself must not be a colon-prefixed line.
if (stripLeadingColon(termLine).isDefinitionLine) return
const termChildren: ElementContent[] = [
...children.slice(0, splitIndex),
...(beforeColon ? [{ type: 'text', value: beforeColon } as Text] : []),
]
const definitionChildren: ElementContent[] = [
...(afterColon ? [{ type: 'text', value: afterColon } as Text] : []),
...children.slice(splitIndex + 1),
]
if (termChildren.length === 0 || definitionChildren.length === 0) return
const lines = splitIntoLines(definitionChildren)
const definitions: ElementContent[][] = []
for (const line of lines) {
const { isNewDefinition, content } = stripLeadingColon(line)
if (isNewDefinition || definitions.length === 0) {
definitions.push(content)
} else {
const current = definitions[definitions.length - 1]
if (current.length > 0 && content.length > 0) {
current.push({ type: 'text', value: ' ' } as Text)
}
current.push(...content)
}
for (const line of restLines) {
if (line.length === 0) continue
const { isDefinitionLine, content } = stripLeadingColon(line)
// Any non-colon-prefixed line means this isn't the definition format.
if (!isDefinitionLine) return
if (content.length > 0) definitions.push(content)
}
const nonEmptyDefinitions = definitions.filter((def) => def.length > 0)
if (nonEmptyDefinitions.length === 0) return
if (definitions.length === 0) return
const dlNode: Element = {
type: 'element',
@ -126,9 +102,9 @@ export const rehypeDefinition: Plugin<[], Root> = () => (tree: Root) => {
type: 'element',
tagName: 'dt',
properties: {},
children: termChildren,
children: termLine,
},
...nonEmptyDefinitions.map(
...definitions.map(
(defChildren): Element => ({
type: 'element',
tagName: 'dd',