Every developer I respect has a place on the internet that’s entirely theirs. Not a Twitter thread, not a LinkedIn post — a corner where they write long-form and think out loud. I wanted that for myself. This is the story of how I built it.
Table of Contents
- Why I Built This
- Why Writing Matters for Developers
- Design — Crafted with Claude
- How It Works — The Tech Stack
- Project Structure Walkthrough
- The Content System — Writing a Post
- Deploying to Cloudflare Pages
- Adding a Custom Domain
- Your First Blog Post
- What’s Next
Why I Built This
I’ve been writing code for nine years — mostly backend stuff, AWS infrastructure, Amazon Connect integrations. Over that time I’ve accumulated a lot of hard-won knowledge: the kind you can’t find in a StackOverflow answer, the kind you only get by shipping things and watching them break in production at 2am.
For the longest time that knowledge lived only in my head, in scattered Notion pages, or in Slack messages that nobody would ever search for again. I kept thinking: someone else is probably fighting this same problem right now. I wanted a place where I could write clearly, share what I know, and maybe save another engineer a few hours of debugging.
This site is that place. It’s my technical journal — a place to write about distributed systems, cloud architecture, frontend patterns, and whatever else I’m working through. If even one post saves you an hour, it was worth the afternoon I spent building this.
Why Writing Matters for Developers
Here’s something nobody tells you early in your career: writing is a career multiplier.
When you write clearly about a technical topic, a few things happen:
You actually understand it better. There’s a concept called the Feynman Technique — if you can’t explain something simply, you don’t really understand it. Writing a blog post forces you to close the gaps. You’ll start drafting a post about, say, DynamoDB single-table design, and realize halfway through that you actually have a fuzzy mental model of partition key selection. Writing exposes that. Then you go fix it.
You build a searchable track record. Resumes are forgettable. A blog post that comes up in Google when someone searches a specific problem? That’s memorable. Recruiters and hiring managers who find your writing already trust you a little before the first interview.
You contribute to a community that gave you a lot. Almost everything I know came from someone else’s blog post, talk, or open source project. Writing is how you pay that back.
You think more carefully before you type. Knowing you might publish something keeps you honest. You double-check assumptions. You think about edge cases. The discipline of writing makes you a sharper thinker at work too.
Start small. It doesn’t have to be a 5,000-word deep dive. Even a post that documents a weird bug you fixed — with the exact error message and the solution — is genuinely useful.
Design — Crafted with Claude
I didn’t start this project with Figma or a mood board. I started with a conversation.
I used Claude (Anthropic’s AI) to design the visual identity of this site. I described what I wanted: minimal, typographic, feels like a technical journal, warm but not childish, readable in both light and dark modes. Through iteration I landed on a design that I genuinely love.
Here’s what defines the aesthetic:
Typography-first. Three fonts, each with a job:
- Newsreader (serif) — for longform prose, gives it a newspaper/journal quality
- Inter (sans-serif) — for UI elements, navigation, labels
- JetBrains Mono (monospace) — for all code blocks
A warm color palette. Most developer blogs go for stark white or pitch black. I wanted something warmer. The background is #f6f3ec — a creamy off-white that’s easy on the eyes for long reading sessions. The dark mode mirrors that warmth.
CSS custom properties everywhere. Rather than a design system framework, I wrote 616 lines of hand-crafted CSS using modern features:
:root {
--bg: #f6f3ec;
--ink: #14110d;
--accent: oklch(62% 0.14 65); /* golden amber */
--serif: "Newsreader", Georgia, serif;
--mono: "JetBrains Mono", monospace;
}
The oklch() color function is fantastic for defining perceptually uniform accent colors. I highly recommend it over hex for anything interactive.
A glassmorphic fixed header. The nav uses backdrop-filter: blur(12px) so the content underneath shows through softly as you scroll. It’s a small touch but it gives the site a polished feel.
No Tailwind, no component library. Everything is hand-written. I wanted full control and zero build-time overhead from a utility framework.
How It Works — The Tech Stack
Astro 5
Astro is a framework for building content-focused websites. The killer feature is its island architecture: ship zero JavaScript by default, opt into JS only for interactive components. For a blog, this is ideal.
The whole site is statically generated at build time. Every page is a .html file in the dist/ folder. No server, no database, no cold starts.
// astro.config.mjs
export default defineConfig({
site: "https://solomonmark.dev",
output: "static",
trailingSlash: "never",
integrations: [react(), sitemap()],
markdown: {
shikiConfig: {
themes: { light: "github-light", dark: "github-dark" },
wrap: false,
},
},
});
TypeScript
The entire project is TypeScript. Astro has excellent TypeScript support out of the box — components are .astro files but props are fully typed, and content collections use Zod for schema validation.
// tsconfig.json
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }
}
}
The @/ alias means I can do import { SITE } from '@/data/site' from anywhere in the project without relative path hell.
Content Collections
Blog posts are Markdown files. Astro’s content collections API gives you type-safe access to frontmatter via a Zod schema:
// src/content/config.ts
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
pubDatetime: z.coerce.date(),
tags: z.array(z.string()),
readTime: z.string().optional(),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
If a .md file is missing a required field, the build fails with a clear error. No silent data corruption.
Dynamic Routes
The src/pages/posts/[slug].astro file handles all blog post URLs. Astro’s getStaticPaths generates one HTML page per post at build time:
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
---
React Islands
The newsletter signup form is the only interactive component on the site. It’s a React component (NewsletterForm.tsx) rendered as an island — it ships its own JavaScript bundle but doesn’t affect the rest of the page:
<!-- Hydrate on client load -->
<NewsletterForm client:load />
Everything else — navigation, post listings, layouts — is pure HTML with no runtime JavaScript overhead.
Syntax Highlighting
Shiki handles code block highlighting at build time. It uses the same engine as VS Code, so the output looks exactly like your editor. Both github-light and github-dark themes are configured, and Astro automatically swaps them based on the active color scheme via CSS.
RSS + Sitemap
Both are auto-generated:
@astrojs/rsscreates/rss.xmlfrom all published posts@astrojs/sitemapcreatessitemap-index.xmlfor SEO
Zero configuration needed beyond adding them to the Astro integrations array.
Project Structure Walkthrough
src/
├── content/
│ ├── blog/ # Markdown files — one file = one post
│ └── config.ts # Zod schema for frontmatter validation
├── pages/
│ ├── index.astro # Home — terminal-style intro + post listing
│ ├── about.astro # Portfolio page
│ ├── posts/[slug].astro # Dynamic route — one page per post
│ └── rss.xml.ts # RSS feed generation
├── layouts/
│ ├── BaseLayout.astro # Root HTML shell — meta, fonts, theme script
│ └── PostLayout.astro # Wraps blog posts with nav + footer
├── components/
│ ├── Chrome.astro # Fixed nav header with theme toggle
│ ├── PostList.astro # Blog listing grid
│ ├── PostFooter.astro # Share buttons, prev/next, tags
│ └── NewsletterForm.tsx # React island for newsletter signup
├── data/
│ └── site.ts # SITE config object — name, socials, skills
├── assets/
│ └── blog/ # Post images
└── styles/
└── global.css # All CSS — 616 lines of custom properties + layouts
The public/ directory holds static files that get copied verbatim to dist/:
public/
├── _headers # Cloudflare HTTP response headers
├── _redirects # Cloudflare URL redirects
└── images/ # Static images
The Content System — Writing a Post
Writing a new post is exactly three steps:
Step 1: Create the file
Create a new .md file in src/content/blog/. The filename becomes the URL slug — my-first-post.md becomes /posts/my-first-post.
Step 2: Add frontmatter
---
title: "Your Post Title"
description: "A one-sentence description that appears in the post listing."
pubDatetime: 2026-04-19
tags: ["tag-one", "tag-two"]
readTime: "5 min read"
draft: false
---
draft: truehides the post from listings and RSS but keeps it accessible by direct URL during developmentpubDatetimesorts the post in the feed — newest firsttagsare rendered as chips on the post and could be used for filtering later
Step 3: Write
Everything below the frontmatter is standard Markdown. Code blocks get syntax-highlighted automatically:
```typescript
const greet = (name: string): string => `Hello, ${name}!`;
```
That’s it. Run npm run dev, visit http://localhost:4321/posts/your-slug, and you’ll see the post rendered in the full site layout.
Deploying to Cloudflare Pages
Cloudflare Pages is the hosting platform I chose. It’s free for personal projects, globally distributed, and integrates directly with GitHub — push to main and your site updates automatically.
Here’s the exact setup:
Step 1: Push your repo to GitHub
Make sure your project is in a GitHub repository. This is what Cloudflare will watch for changes.
Step 2: Create a Cloudflare account
Sign up at cloudflare.com if you don’t have an account.
Step 3: Connect your repo to Cloudflare Pages
- Go to your Cloudflare dashboard
- Navigate to Workers & Pages → Pages
- Click Create a project
- Click Connect to Git
- Authorize Cloudflare to access your GitHub account
- Select your repository from the list
Step 4: Configure the build settings
In the “Set up builds and deployments” screen, set:
| Setting | Value |
|---|---|
| Production branch | main |
| Build command | npm run build |
| Build output directory | dist |
| Node.js version | 20 (set in Environment variables: NODE_VERSION = 20) |
Step 5: Deploy
Click Save and Deploy. Cloudflare will:
- Clone your repository
- Run
npm install && npm run build - Upload the
dist/contents to their global CDN - Give you a
*.pages.devURL immediately
Every subsequent push to main triggers an automatic redeploy. Pull requests get their own preview URL, which is great for reviewing changes before they go live.
Adding a Custom Domain
Once deployed, your site lives at something like solomonmark-dev.pages.dev. Here’s how to point your own domain at it:
If your domain is already on Cloudflare
- Go to Workers & Pages → your project → Custom domains
- Click Set up a custom domain
- Enter your domain (e.g.,
solomonmark.dev) - Click Continue — Cloudflare automatically adds the DNS record
- Done. Propagation takes seconds since DNS is already on Cloudflare.
If your domain is registered elsewhere
- In your Cloudflare Pages project → Custom domains → Set up a custom domain
- Enter your domain and click Continue
- Cloudflare gives you a
CNAMErecord to add:Type: CNAME Name: www (or @ for root) Target: solomonmark-dev.pages.dev - Go to your domain registrar’s DNS settings and add that record
- Propagation takes a few minutes to a few hours
Cloudflare automatically provisions a TLS certificate (HTTPS) for your custom domain. No setup needed.
The wrangler.toml file
For local development and CLI-based deployments, the wrangler.toml config is:
name = "solomonmark-dev"
compatibility_date = "2024-09-23"
[assets]
directory = "./dist"
You can also deploy manually with:
npm run build
npx wrangler pages deploy dist --project-name=solomonmark-dev
Your First Blog Post
Here’s a complete, working example of a first blog post you can drop into this project:
Create the file src/content/blog/hello-world.md:
---
title: "Hello, World"
description: "My first post — a quick note on why I'm starting to write."
pubDatetime: 2026-04-19
tags: ["meta", "writing"]
readTime: "2 min read"
draft: false
---
I've been meaning to start writing for a long time.
Every time I solved a tricky problem at work, I'd think: *I should write this down*.
Every time I spent three hours debugging something that turned out to have a one-line
fix, I'd think: *someone else is going to hit this*. And then I'd move on.
This blog is me finally making good on that impulse.
## What I'll Write About
Mostly backend and cloud stuff — the things I spend most of my time on:
- AWS architecture and serverless patterns
- Amazon Connect and contact center integrations
- TypeScript and Node.js patterns I've found useful
- Distributed systems concepts (with examples, not just theory)
- The occasional frontend deep dive
## What I Won't Do
Write just to post. If I don't have something worth saying, I'll wait until I do.
---
If something I write saves you an hour of debugging, drop me a note at
[hello@solomonmark.dev](mailto:hello@solomonmark.dev). That's the whole point.
When you run npm run dev and visit /posts/hello-world, you’ll see this rendered in the full site layout — title, date, read time, tags, share buttons, and all.
What’s Next
The site is live and working, but there’s always more to build. A few things on my list:
Tag filtering — right now tags are decorative. I want clicking a tag to filter the post list.
Search — with enough posts, discoverability becomes a real problem. I’m looking at Pagefind, which does static full-text search with zero server.
Analytics — privacy-respecting analytics (Cloudflare Web Analytics or Fathom) to understand which posts are actually useful to people.
Newsletter backend — the signup form currently doesn’t do anything. I need to wire it up to something like Resend + a simple KV store.
If you want to build something like this yourself, the full source is on GitHub at github.com/Solomon-m/solomonmarkdev. Clone it, break it, make it yours.
Questions? I’m at hello@solomonmark.dev.
Credit & Thanks
A special thank you to Peter Steinberger — this blog is directly inspired by his personal site steipete.me. Peter’s willingness to open-source his work and share it with the community is exactly the spirit I want to carry forward with my own writing. If you like what you see here, go check out his site — it’s the original.