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 { getAllPostsMeta } from '@/lib/posts'
import { Post } from '@/components/post/Post' import { Post } from '@/components/post/Post'
export default function BlogIndex() { export default function Homepage() {
const posts = getAllPostsMeta() const posts = getAllPostsMeta()
const rest = posts.slice(1) const rest = posts.slice(1)

View file

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

View file

@ -1,16 +1,41 @@
--- ---
title: About 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' import { visit } from 'unist-util-visit'
/** /**
* Strips a leading match from the first text node in the given children array. * Checks whether a line starts with a colon marker (optionally preceded by
* @param children The children array to process. * whitespace) and, if so, strips it and returns the remaining content.
* @param regex The regex pattern to match and strip from the first text node. * @param line The line (array of ElementContent) to check.
* @returns The modified children array with the leading match stripped. * @returns Whether the line is a colon-prefixed definition line, and its content with the marker stripped.
*/ */
const stripLeadingColon = ( const stripLeadingColon = (
line: ElementContent[], line: ElementContent[],
): { isNewDefinition: boolean; content: ElementContent[] } => { ): { isDefinitionLine: boolean; content: ElementContent[] } => {
const [first, ...rest] = line const [first, ...rest] = line
if (first?.type === 'text') { if (first?.type === 'text') {
const match = first.value.match(/^[ \t]*:[ \t]*/) const match = first.value.match(/^[ \t]*:[ \t]*/)
if (match) { if (match) {
const remainder = first.value.slice(match[0].length) const remainder = first.value.slice(match[0].length)
return { return {
isNewDefinition: true, isDefinitionLine: true,
content: remainder ? [{ type: 'text', value: remainder } as Text, ...rest] : rest, 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 * A rehype plugin that transforms paragraphs shaped like:
* 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 * Term
* definition. * : 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) => { export const rehypeDefinition: Plugin<[], Root> = () => (tree: Root) => {
visit(tree, 'element', (node: Element, index, parent) => { visit(tree, 'element', (node: Element, index, parent) => {
if (!parent || typeof index !== 'number') return if (!parent || typeof index !== 'number') return
if (node.tagName !== 'p') return if (node.tagName !== 'p') return
const children = node.children const lines = splitIntoLines(node.children)
let splitIndex = -1 // Need at least a term line and one definition line.
let colonIndex = -1 if (lines.length < 2) return
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
}
}
}
if (splitIndex === -1) return const [termLine, ...restLines] = lines
const splitText = children[splitIndex] as Text if (termLine.length === 0) return
const beforeColon = splitText.value.slice(0, colonIndex) // The term line itself must not be a colon-prefixed line.
const afterColon = splitText.value.slice(colonIndex + 1).replace(/^[ \t]+/, '') 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[][] = [] const definitions: ElementContent[][] = []
for (const line of lines) { for (const line of restLines) {
const { isNewDefinition, content } = stripLeadingColon(line) if (line.length === 0) continue
const { isDefinitionLine, content } = stripLeadingColon(line)
if (isNewDefinition || definitions.length === 0) { // Any non-colon-prefixed line means this isn't the definition format.
definitions.push(content) if (!isDefinitionLine) return
} else { if (content.length > 0) definitions.push(content)
const current = definitions[definitions.length - 1]
if (current.length > 0 && content.length > 0) {
current.push({ type: 'text', value: ' ' } as Text)
}
current.push(...content)
}
} }
const nonEmptyDefinitions = definitions.filter((def) => def.length > 0) if (definitions.length === 0) return
if (nonEmptyDefinitions.length === 0) return
const dlNode: Element = { const dlNode: Element = {
type: 'element', type: 'element',
@ -126,9 +102,9 @@ export const rehypeDefinition: Plugin<[], Root> = () => (tree: Root) => {
type: 'element', type: 'element',
tagName: 'dt', tagName: 'dt',
properties: {}, properties: {},
children: termChildren, children: termLine,
}, },
...nonEmptyDefinitions.map( ...definitions.map(
(defChildren): Element => ({ (defChildren): Element => ({
type: 'element', type: 'element',
tagName: 'dd', tagName: 'dd',