<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Kyles Blog</title>
  <link href="https://rusby.blog" rel="alternate"/>
  <link href="https://rusby.blog/feed.xml" rel="self"/>
  <id>https://rusby.blog/</id>
  <updated>2026-04-07T18:16:28+00:00</updated>
  <entry>
    <title>I Built a Blogging Platform in a Weekend. Here’s Everything That Happened.</title>
    <link href="https://rusby.blog/i-built-a-blogging-platform-in-a-weekend-heres-everything-that-happened"/>
    <id>https://rusby.blog/i-built-a-blogging-platform-in-a-weekend-heres-everything-that-happened</id>
    <updated>2026-04-07T18:16:28+00:00</updated>
    <content type="html">&lt;p&gt;Over one weekend, I shipped a complete blogging platform from first line of code to production. It’s powered by the Notion API and includes AI-generated SEO, multiple themes, custom domains, Stripe billing, and privacy-friendly analytics, built solo with a stack I know well: Notion, Laravel, and a lot of stubbornness.&lt;/p&gt;&lt;p&gt;This is the story of building tiniest.blog.&lt;/p&gt;&lt;h2 id=&quot;the-idea&quot;&gt;The Idea&lt;/h2&gt;&lt;p&gt;I write in Notion. Most of my notes, drafts, and half-formed ideas live there. But publishing those ideas as a blog always meant migrating content to WordPress, Ghost, or Substack, copying text, reformatting, fiddling with settings, losing the Notion workflow I’d already built.&lt;/p&gt;&lt;p&gt;The question I kept coming back to was simple: what if I could point at a Notion database and have a blog appear? I didn’t want a brittle export pipeline or a script I’d need to babysit. I wanted a real blog that stayed in sync automatically, shipped with proper SEO, supported a custom domain, and loaded fast enough to feel like a static site generator.&lt;/p&gt;&lt;h2 id=&quot;day-1-the-foundation&quot;&gt;Day 1: The Foundation&lt;/h2&gt;&lt;p&gt;The first commit landed on Saturday afternoon. A design document, a monochrome editorial design system, and a 19-task implementation plan. I knew the shape of what I was building before I wrote a line of application code.&lt;/p&gt;&lt;p&gt;The stack: Laravel 13 for the backend, React with TypeScript via Inertia.js for the dashboard, PostgreSQL, S3 for static file storage. No server-side rendering for blog pages, the output would be pure static HTML. Zero JavaScript on the public blog. Under 8KB of CSS.&lt;/p&gt;&lt;p&gt;The first real challenge was the Notion API. Notion’s block structure is deeply nested, a page contains blocks, blocks contain children, children contain more children. A table of contents block needs to scan all sibling headings. A column layout needs to know how many columns exist. A synced block references content from somewhere else entirely.&lt;/p&gt;&lt;p&gt;I built a &lt;code&gt;NotionBlockRenderer&lt;/code&gt; that handles every Notion block type: paragraphs, headings, lists, code blocks, tables, toggles, callouts, equations, bookmarks, embeds, columns, and more. Each block maps to semantic HTML. The renderer walks the tree recursively, handles nested children, and produces clean, accessible markup.&lt;/p&gt;&lt;p&gt;By the end of day one, I had:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Notion OAuth for connecting workspaces&lt;/li&gt;&lt;li&gt;A full sync pipeline (fetch pages, render blocks, generate HTML)&lt;/li&gt;&lt;li&gt;Static blog output with a proper editorial design&lt;/li&gt;&lt;li&gt;An onboarding flow that goes from “nothing” to “live blog” in three steps&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;day-2-the-hard-parts&quot;&gt;Day 2: The Hard Parts&lt;/h2&gt;&lt;p&gt;On Sunday I focused on the things that separate a prototype from something that feels like a product.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;AI-generated SEO.&lt;/strong&gt; Every blog post gets automatic meta tags, Open Graph data, JSON-LD structured data, and keyword extraction, all generated by an AI agent that reads the content and produces structured output. No SEO knowledge required from the user. The blog just ranks.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Multi-blog support.&lt;/strong&gt; The initial design was one blog per user. By Sunday morning, I’d rearchitected the entire data model: users can have multiple blogs, each connected to different Notion databases, potentially from different Notion workspaces. This meant rewriting controllers, policies, and the entire routing system.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The image problem.&lt;/strong&gt; Notion’s image URLs expire after one hour. Every image in every blog post would break within 60 minutes of syncing. The fix: an image proxy that downloads every Notion image to our own S3 bucket during sync, rewrites the URLs in the HTML, and serves them permanently.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Themes.&lt;/strong&gt; I started with one monochrome editorial theme. By the end of the day, I had six: Default (Fraunces serif), Warm (Playfair Display, earthy tones), Ocean (DM Sans, cool blues), Brutalist (Space Mono, raw and bold), Pastel (Nunito, soft colours), and Night (JetBrains Mono, terminal green on black). Each theme is a single CSS file under 8KB. Each supports dark mode automatically via &lt;code&gt;prefers-color-scheme&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Layouts.&lt;/strong&gt; Four index layouts (list, grid, magazine, minimal) and three post layouts (standard, wide, sidebar). Mix and match with any theme. All pure CSS, no JavaScript.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Stripe billing.&lt;/strong&gt; Free tier gets 3 blogs with 10 posts each. Pro unlocks unlimited everything plus custom domains. Cashier handles subscriptions, checkout, and the billing portal.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Privacy analytics.&lt;/strong&gt; Page views recorded server-side, no cookies, no JavaScript tracker, no GDPR consent banner needed. Just path, referrer, and country, with IP addresses hashed using a daily rotating salt.&lt;/p&gt;&lt;h2 id=&quot;day-3-production-and-the-domain-problem&quot;&gt;Day 3: Production and the Domain Problem&lt;/h2&gt;&lt;p&gt;By Monday I was ready to put it in production, using Laravel Cloud for hosting and Cloudflare for DNS.&lt;/p&gt;&lt;p&gt;The first deployment worked, and subdomain routing (&lt;code&gt;kyle.tiniest.blog&lt;/code&gt;) resolved correctly. The blog loaded, the themes rendered as expected, and I got that brief satisfaction of something working on the first try.&lt;/p&gt;&lt;p&gt;After that, I moved on to custom domains.&lt;/p&gt;&lt;p&gt;Custom domains on a platform like this are genuinely hard. The user adds &lt;code&gt;rusby.blog&lt;/code&gt; to their settings. We need to issue an SSL certificate for that domain, route traffic to our servers, and resolve the request to the correct blog, all automatically, for any domain, without manual intervention.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Attempt 1: DNS verification.&lt;/strong&gt; Check if the user has set a CNAME record pointing to us. This works, but Cloudflare’s proxy strips the original hostname, and browsers throw &lt;code&gt;Error 1014: CNAME Cross-User Banned&lt;/code&gt; when the CNAME crosses between Cloudflare accounts.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Attempt 2: Cloudflare for SaaS.&lt;/strong&gt; This is the right solution, Cloudflare’s product for exactly this use case. Register each custom domain as a “custom hostname” via their API, and Cloudflare handles SSL provisioning automatically. The fallback origin points to &lt;code&gt;custom-domains.tiniest.blog&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Attempt 3: But Laravel Cloud only accepts &lt;/strong&gt;&lt;strong&gt;&lt;code&gt;*.tiniest.blog&lt;/code&gt;&lt;/strong&gt;&lt;strong&gt;.&lt;/strong&gt; When &lt;code&gt;rusby.blog&lt;/code&gt; arrives at Cloudflare’s edge and gets routed to our origin, the &lt;code&gt;Host&lt;/code&gt; header is still &lt;code&gt;rusby.blog&lt;/code&gt;. Laravel Cloud rejects it, it only allows hostnames matching the configured domain.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The fix: a Cloudflare Worker.&lt;/strong&gt; A 20-line JavaScript function that intercepts custom domain requests, rewrites the &lt;code&gt;Host&lt;/code&gt; header to &lt;code&gt;custom-domains.tiniest.blog&lt;/code&gt;, and preserves the original domain in an &lt;code&gt;X-Original-Host&lt;/code&gt; header. Laravel reads that header and resolves the correct blog.&lt;/p&gt;&lt;p&gt;In practice it didn’t work on the first try, and &lt;code&gt;rusby.blog&lt;/code&gt; kept returning a 404.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;X-Original-Host&lt;/code&gt; header was being set by the Worker, but Laravel Cloud’s load balancer was overwriting &lt;code&gt;X-Forwarded-Host&lt;/code&gt; with the rewritten hostname. The original domain was lost somewhere in the proxy chain.&lt;/p&gt;&lt;p&gt;It took about an hour to diagnose, and the eventual fix was a single line. I added &lt;code&gt;X-Original-Host&lt;/code&gt; as a custom header that load balancers don’t touch, and checked for it first in the middleware. Once that was in place, requests started resolving to the right blog.&lt;/p&gt;&lt;h2 id=&quot;the-security-audit&quot;&gt;The Security Audit&lt;/h2&gt;&lt;p&gt;Before calling it done, I ran a full security audit, four parallel agents examining authentication, injection vectors, authorization, and infrastructure hardening.&lt;/p&gt;&lt;p&gt;They found 15 issues. Three critical:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Stored XSS via Notion links.&lt;/strong&gt; The block renderer HTML-encoded URLs but didn’t validate the scheme. A &lt;code&gt;javascript:&lt;/code&gt; URL would pass through &lt;code&gt;e()&lt;/code&gt; untouched and execute in the browser. Fixed with a URL sanitizer that only allows &lt;code&gt;http://&lt;/code&gt;, &lt;code&gt;https://&lt;/code&gt;, &lt;code&gt;mailto:&lt;/code&gt;, and &lt;code&gt;tel:&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Webhook authentication bypass.&lt;/strong&gt; The Notion webhook endpoint silently accepted requests when the webhook secret wasn’t configured, a fail-open pattern. Changed to fail-closed.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Header spoofing on custom domains.&lt;/strong&gt; Any direct request to the origin could set &lt;code&gt;X-Original-Host&lt;/code&gt; to impersonate any blog. Fixed with a shared secret between the Cloudflare Worker and Laravel.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Plus 12 more: HSTS headers, Content Security Policy, rate limiting on registration and domain creation, mass assignment hardening, destructive command protection, password policy fallbacks. All fixed in a single commit.&lt;/p&gt;&lt;h2 id=&quot;more-themes-more-layouts&quot;&gt;More Themes, More Layouts&lt;/h2&gt;&lt;p&gt;Research across Ghost, WordPress, and Substack, plus 2026 design trend reports, showed clear demand for more variety. I added four more themes:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Terminal&lt;/strong&gt;, IBM Plex Mono, matrix green on black. For developers who want their blog to feel like a terminal.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Serif Classic&lt;/strong&gt;, Playfair Display and Merriweather on warm cream. The New Yorker meets Substack.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Swiss&lt;/strong&gt;, Inter with a red accent. International Typographic Style, clean, grid-based, timeless.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Meadow&lt;/strong&gt;, DM Serif Display with sage green tones. Warm minimalism, 2026’s biggest design trend.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Two new index layouts: Newsletter (date-focused, Substack-style) and Masonry (Pinterest-style cards). Two new post layouts: Longform (wider column with drop caps) and Split (side-by-side hero image and title).&lt;/p&gt;&lt;p&gt;10 themes. 6 index layouts. 5 post layouts. 300 possible combinations, all from pure CSS.&lt;/p&gt;&lt;h2 id=&quot;what-tiniestblog-is-now&quot;&gt;What tiniest.blog Is Now&lt;/h2&gt;&lt;p&gt;Three days of work produced:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;A complete multi-tenant blogging platform&lt;/strong&gt; built on Laravel 13 and React&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Real-time Notion sync&lt;/strong&gt; via webhooks with a polling fallback&lt;/li&gt;&lt;li&gt;&lt;strong&gt;AI-generated SEO&lt;/strong&gt; for every post, meta tags, JSON-LD, Open Graph, keywords&lt;/li&gt;&lt;li&gt;&lt;strong&gt;10 CSS themes&lt;/strong&gt; and &lt;strong&gt;11 layout options&lt;/strong&gt;, all under 8KB, zero JavaScript&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Custom domains&lt;/strong&gt; via Cloudflare for SaaS with automatic SSL&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Privacy analytics&lt;/strong&gt;, no cookies, no tracking scripts&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Stripe billing&lt;/strong&gt; with free and Pro tiers&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Static HTML output&lt;/strong&gt; served from Cloudflare R2, sub-second page loads&lt;/li&gt;&lt;li&gt;&lt;strong&gt;162 commits&lt;/strong&gt;, comprehensive test coverage, and a full security audit&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The architecture is simple: you write in Notion, we sync and build static HTML, your readers get the fastest blog on the internet. No migration. No lock-in. Your content stays in Notion where it belongs.&lt;/p&gt;&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next&lt;/h2&gt;&lt;p&gt;At this point the product works and the infrastructure feels solid, but the next challenge is distribution.&lt;/p&gt;&lt;p&gt;The go-to-market is Reddit-first, r/Notion, r/SideProject, r/webdev, followed by a Product Hunt launch and outreach to Notion creators. The pitch writes itself: “Turn your Notion database into a beautiful blog in 60 seconds.”&lt;/p&gt;&lt;p&gt;If you write in Notion and want a blog without the overhead, tiniest.blog is live. Connect your workspace, pick a database, and your blog exists.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;Built by Kyle Rusby. Powered by Notion, Laravel, and an unreasonable amount of concentration 👀&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Try it free at &lt;/em&gt;&lt;a href=&quot;https://tiniest.blog/&quot;&gt;&lt;em&gt;tiniest.blog&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Kyle’s Software Engineering Blog. Building Simple, Sturdy Products with tiniest.blog</title>
    <link href="https://rusby.blog/kyles-software-engineering-blog-building-simple-sturdy-products-with-tiniestblog"/>
    <id>https://rusby.blog/kyles-software-engineering-blog-building-simple-sturdy-products-with-tiniestblog</id>
    <updated>2026-04-07T18:16:25+00:00</updated>
    <content type="html">&lt;p&gt;Hi, I’m Kyle.&lt;/p&gt;&lt;p&gt;Welcome to my &lt;strong&gt;software engineering blog&lt;/strong&gt;, where I share software engineering lessons from building and shipping real products. I like making things that feel simple, sturdy, and pleasant to use. I also like writing, usually in short bursts, usually when something has just clicked, or just broken, or just surprised me.&lt;/p&gt;&lt;p&gt;If you arrived here from search, here is the quick context. I built &lt;a href=&quot;http://tiniest.blog/&quot;&gt;tiniest.blog&lt;/a&gt; to make publishing blog posts genuinely easy. It is for people who want to write and hit publish without turning it into a weekend project. Less setup. Less fiddling. More writing.&lt;/p&gt;&lt;p&gt;If you’re here, grab a tea or a coffee, and stay for one post, or wander around for a while.&lt;/p&gt;&lt;h3 id=&quot;why-i-write-software-engineering-lessons&quot;&gt;Why I write software engineering lessons&lt;/h3&gt;&lt;p&gt;My brain works better when I put ideas into words. Even a small note can turn a fuzzy thought into something you can hold.&lt;/p&gt;&lt;p&gt;Some posts will be practical. Things I tried, what worked, what did not, and what I would do differently next time. Some will be reflective. What it feels like to build software products for real people, under real constraints, with limited time and imperfect information.&lt;/p&gt;&lt;p&gt;I am not trying to be a guru. I am not trying to win the internet. I am mostly trying to be useful to my future self, and hopefully to you as well.&lt;/p&gt;&lt;h3 id=&quot;software-engineering-lessons-and-topics-covered&quot;&gt;Software engineering lessons and topics covered&lt;/h3&gt;&lt;p&gt;Most of what I write will be about building software products. That includes the craft of engineering, and the choices that make a product feel calm and reliable.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Building and shipping software products&lt;/li&gt;&lt;li&gt;Tools and workflows that reduce friction&lt;/li&gt;&lt;li&gt;Small design decisions that add up&lt;/li&gt;&lt;li&gt;Bugs, fixes, and lessons learned&lt;/li&gt;&lt;li&gt;The boring fundamentals that keep things running&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Sometimes the writing will be technical. Sometimes it will be plain language. I like both. I like code, and I like the human side of building things.&lt;/p&gt;&lt;p&gt;I also care about the pace of work. The goal is not to sprint forever. The goal is to keep going. So you might see posts about habits, focus, energy, and the small routines that make a week feel manageable.&lt;/p&gt;&lt;h3 id=&quot;a-warm-practical-tone&quot;&gt;A warm, practical tone&lt;/h3&gt;&lt;p&gt;I aim for clarity, but I do not want this place to feel stiff.&lt;/p&gt;&lt;p&gt;Software can be serious, but it can also be playful. There is joy in getting a tiny detail right. There is relief in deleting a big chunk of code. There is comedy in the way a one line change can break everything.&lt;/p&gt;&lt;p&gt;So I will try to keep things warm. I will keep it honest. I will keep it readable.&lt;/p&gt;&lt;p&gt;If I change my mind about something, I will say so. If I make a mistake, I will correct it. If you spot something off, feel free to tell me.&lt;/p&gt;&lt;h3 id=&quot;about-tiniestblog&quot;&gt;About &lt;a href=&quot;http://tiniest.blog/&quot;&gt;tiniest.blog&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;http://tiniest.blog/&quot;&gt;tiniest.blog&lt;/a&gt; is my attempt to remove the usual barriers to blogging.&lt;/p&gt;&lt;p&gt;A lot of writing tools make you do a pile of setup before you get to the part that matters. Pick a theme. Pick a layout. Pick a framework. Pick a hosting provider. Pick a build pipeline. Then, maybe, write.&lt;/p&gt;&lt;p&gt;I wanted the opposite. I wanted a short path from thought to published post.&lt;/p&gt;&lt;p&gt;I have noticed that good habits often fail because the cost is too high in the moment. Not because people are lazy. Because life is busy, and the barrier is just a bit too tall.&lt;/p&gt;&lt;p&gt;So I try to lower the barrier. If you want to write more, it helps when the tool gets out of the way. That is what I am aiming for with &lt;a href=&quot;http://tiniest.blog/&quot;&gt;tiniest.blog&lt;/a&gt;, and it is what I am aiming for with this software engineering blog too.&lt;/p&gt;&lt;h3 id=&quot;short-posts-complete-thoughts&quot;&gt;Short posts, complete thoughts&lt;/h3&gt;&lt;p&gt;This is not a place where every post needs to be a masterpiece.&lt;/p&gt;&lt;p&gt;Some posts will be a single idea, cleanly stated. Some will be a small tutorial. Some will be a note-to-self that turns out to be useful for someone else.&lt;/p&gt;&lt;p&gt;My only rule is that I want to publish complete thoughts. Even if a post is short, it should land the point. If I do not have the ending yet, I would rather wait and finish it than ship something half done.&lt;/p&gt;&lt;h3 id=&quot;thanks-for-stopping-by&quot;&gt;Thanks for stopping by&lt;/h3&gt;&lt;p&gt;If you want to follow along, start anywhere. Pick a title that looks interesting and see if it is your thing.&lt;/p&gt;&lt;p&gt;I will keep building. I will keep learning. I will keep writing.&lt;/p&gt;&lt;p&gt;And genuinely, thanks for reading :)&lt;/p&gt;</content>
  </entry>
</feed>