From 1a5ea2f6c02d81b2a6fdeb4d447797fdd47c48d4 Mon Sep 17 00:00:00 2001 From: HangerThem Date: Tue, 4 Aug 2026 08:52:43 +0200 Subject: [PATCH] Fixed few SSG issues and cleanedup about me --- app/(withRecent)/page.tsx | 2 +- app/about/page.tsx | 14 +----- content/pages/about.md | 39 ++++++++++++++--- lib/rehype-definition.tsx | 92 +++++++++++++++------------------------ 4 files changed, 69 insertions(+), 78 deletions(-) diff --git a/app/(withRecent)/page.tsx b/app/(withRecent)/page.tsx index b407eb1..9e90794 100644 --- a/app/(withRecent)/page.tsx +++ b/app/(withRecent)/page.tsx @@ -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) diff --git a/app/about/page.tsx b/app/about/page.tsx index 296587b..b4e0012 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -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 { const { meta } = await getPostBySlug('about', 'content/pages') @@ -29,7 +19,7 @@ export async function generateMetadata(): Promise { } export default async function Post() { - const { meta, content } = await getPostBySlug('about', 'content/pages') + const { meta, content, tableOfContents } = await getPostBySlug('about', 'content/pages') return (
@@ -39,7 +29,7 @@ export default async function Post() {
{content}
-
+
) } diff --git a/content/pages/about.md b/content/pages/about.md index ac8cc7b..e427bd3 100644 --- a/content/pages/about.md +++ b/content/pages/about.md @@ -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). \ No newline at end of file +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). \ No newline at end of file diff --git a/lib/rehype-definition.tsx b/lib/rehype-definition.tsx index 791c757..615948e 100644 --- a/lib/rehype-definition.tsx +++ b/lib/rehype-definition.tsx @@ -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
element with a
for the term and a
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
element with a
for the term and a
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',