root@construct:~/projects/broad-way$
<-- back to /projects

The Broad Way

This blog: terminal-first and bilingual

Next.js 16React 19Tailwind v4TypeScript

>Context

The Broad Way is kinho.dev: my personal site and engineering blog, in English and Portuguese. The name comes from a Miyamoto Musashi line the site prints in the browser console: "When you know the way broadly you see it in all things."

I write about LLM agents, architecture and production work, in three sections: logs, rants and nonsense. The site also holds career and contact (/about), projects, consulting, the tools I use every day (/loadout) and my résumé as a D&D 5e character sheet (/sheet).

I started it on 20 November 2025 from create-next-app, and the site is live at kinho.dev.

>High-level design

It runs entirely on Vercel, on Next.js 16 with the App Router, React 19 and Tailwind v4. Every GitHub pull request gets a preview, and merging to main ships to production.

A request without a language goes through the middleware, which reads Accept-Language and redirects to /pt-BR or /en-US. From there it lands in one of two route groups with separate root layouts: (site), with the terminal navigation, or (sheet), the D&D sheet with its own HTML, fonts and styles.

There is no database: each post is a TypeScript module per language, and a single catalog hides drafts and feeds the pages, RSS and sitemap. The only path with external state is contact: the browser posts JSON to /api/contact, which validates, drops bots, rate-limits per IP and delivers in parallel to a GitHub issue and, when configured, an email through Resend.

Hover or focus a piece: it lights up with its links.
who uses itVercelcode and deliveryexternal services1 Visitor3 Site and blog2 Middleware6 Post catalog4 D&D sheet8 GitHub repository5 Contact API9 GitHub Issues7 RSS and sitemap10 Resend
10 pieces9 links4 areas

Components

  • Locale middleware :: Redirects URLs without a language to /en-US or /pt-BR based on Accept-Language, with en-US as the default.
  • Post catalog :: One TypeScript module per post and language; a single function keeps drafts out of pages, RSS and the sitemap.
  • Feeds :: RSS at /en-US/rss.xml, /pt-BR/rss.xml and /rss.xml, and a sitemap built from the same catalog.
  • Character sheet (/sheet) :: A route group with its own root layout, four fonts of its own and a canvas-drawn d20 that respects reduced motion.
  • Contact API :: Validates the fields, drops bots with a honeypot, rate-limits per IP and delivers through an issue in a private repository plus optional email.
  • Tests :: Unit tests with node:test for the catalog, drafts, feeds, contact and sheet, plus Playwright browser suites.

>Trade-offs

The D&D sheet lives in its own route group, with an independent html and body.

Alternative rejected: Extending the framed site page, or hiding the frame with CSS.

Why: Separate route groups give an independent html and body and avoid rendering hidden, inaccessible site navigation.

A draft is a flag on the post, and a single catalog decides what shows.

Alternative rejected: Filtering drafts in each place: home, lists, post pages, RSS and sitemap.

Why: One filter covers all of them, and the tests check that a draft's title, URL and body never reach the RSS or the sitemap. The preview flag is not access control, and the README says so.

An explicit slug when a title gets corrected.

Alternative rejected: Always deriving the slug from the title.

Why: Fixing a title would change the published URL. Pinning the slug to the original keeps links already out there working.

Contact delivered through a GitHub issue as the main channel, with optional email. It counts as delivered if any channel accepts.

Alternative rejected: Relying on a single email send.

Why: Each message becomes an issue and arrives as a GitHub notification. If no channel accepts, the API returns 502 and the page shows the email address and LinkedIn, so a message never disappears silently.

>Highlights

  • >62 published posts per language (27 logs, 29 rants, 6 nonsense), from 20 Nov 2025 to 25 Sep 2026, plus 5 drafts kept offline.
  • >The /en-US RSS feed had 62 items on 22 Oct 2026.
  • >Unit tests with node:test, plus 4 Playwright browser test files.
  • >Contact with a honeypot, a limit of 5 messages per IP every 10 minutes, and messages up to 5,000 characters.
  • >The sheet's d20 is 385 lines of TypeScript on a 2D canvas: an icosahedron, quaternions and one shared animation loop for every die.

>Fun facts

  • *Open the browser console on kinho.dev: an ASCII katana shows up with the Musashi line that named the site.
  • *On the sheet's d20, the faces are numbered so opposite faces add up to 21, like a real die.
  • *On the sheet I am a level 8 Tech Samurai.
  • *The site's package.json is named chrono-aldrin.
The Broad Way | The Broad Way | Kinho.dev