Fixed few SSG issues and cleanedup about me
This commit is contained in:
parent
ff553187db
commit
1a5ea2f6c0
4 changed files with 69 additions and 78 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue