Rohan's Blog

Hello, world (and a tour of this blog)

What this site is, why I'm running it as a sibling to the portfolio, and a quick tour of every feature so I remember what's in here.

5 min read
On this page

This is the first real post. It exists mostly to exercise every feature — code blocks, table-of-contents, related posts, smart quotes, the works — so I can spot regressions later just by reading it through.

If you’re here for content, the rest of the archive is more useful. If you’re here because you’re building your own Astro blog and stole this template: welcome, the README will save you an evening.

Why a separate blog

I already have a portfolio at rohansri.com. That site is built around projects — Falcon 9 landing prediction, a boxing-odds tool, a password generator — and it’s loud on purpose: extra-bold display type, navy, photographs, motion. It’s a fine surface for showing off, but it’s a bad surface for reading 2,000 words about load balancers.

So the blog is a sibling. Same design tokens — same Inter, same navy #1e3a8a, same warm cream #ede8df lifted from the bento media section — but tuned for reading. No hero carousel, no flashing accents, just a narrow column and good line height.

What “same family” means here

A few rules I’m trying to keep myself honest about:

  • Tokens are declared once, in src/styles/global.css, inside the Tailwind @theme block. Nothing in a component references a raw hex value.
  • The blog uses the cream canvas as default. The portfolio uses white plus sectional blocks of cream and deep navy. Same vocabulary, different default chord.
  • Type weights are limited to 400 / 700 / 800. Anything else would muddy the resemblance.

Code blocks

Shiki is wired up with github-dark and a handful of transformers, so I can mark lines as highlighted, added, or removed using the inline-comment syntax.

export async function getPublishedPosts(): Promise<Post[]> {
  const all = await getCollection('blog', ({ data }) => {
    return import.meta.env.PROD ? !data.draft : true; 
  });
  return all.map(withReadingTime).sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
  );
}

Diff syntax also works for showing the before/after of a refactor:

function getRelated(current: Post, all: Post[]) {
  const others = all.filter((p) => p.id !== current.id); 
  const others = all.filter((p) => p.id !== current.id && !p.data.draft); 
  return others;
}

A shell snippet — language label and copy-button get added by a tiny client script in PostLayout.astro:

npm create astro@latest
npm install
npm run dev

And a JSON snippet, because every blog post template needs one:

{
  "site": "blog.rohansri.com",
  "framework": "Astro 5",
  "css": "Tailwind 4",
  "icons": "astro-icon"
}

Inline code and quotes

You can also drop inline code mid-sentence, and the smart-typography pass means “straight quotes” turn into curly ones automatically — that’s smartypants: true in astro.config.mjs.

A reading site is mostly absence. The good stuff is what you didn’t put on the page.

Table of contents

The post layout renders a TOC whenever a post has three or more H2 sections. On desktop (lg+) it floats as a sticky rail on the right of the column. On smaller screens it collapses into a <details> element at the top.

The scroll-spy uses IntersectionObserver with a tight root margin so the active link tracks roughly where the reader is actually looking, not where the heading first enters the viewport.

Why three is the threshold

Two H2s don’t need a TOC — at that point it’s noise. Three is when “I’d like to jump to part 3” becomes a real impulse. This is a vibes call, not science.

At the bottom of every post:

  • Up to three related posts, ranked by shared-tag count. If a post doesn’t have enough tag overlap, the list falls back to the most recent published posts so the section never looks empty.
  • A prev/next pair, where “next” means a newer post and “prev” means an older one. This is the convention I find least confusing.
  • /rss.xml — generated by @astrojs/rss. Drafts excluded.
  • /sitemap-index.xml — generated by @astrojs/sitemap. OG image endpoints are filtered out.
  • /search — client-side full-text search using Fuse.js. The index lives at /search-index.json and is built at compile time from titles, descriptions, tags, and the first ~500 characters of post body (with code fences stripped so the index isn’t full of } and ;).

Drafts

draft: true in frontmatter means a post shows up in npm run dev but is excluded from the production build, from RSS, from the sitemap, from the search index, from tag pages, and from related-posts lookups. There’s exactly one helper — getPublishedPosts() — that enforces this, so adding a new page won’t accidentally leak drafts.

See draft-tour.mdx for an example. Run npm run build and you’ll notice it doesn’t appear in dist/.

What’s not here

No comments, no analytics, no newsletter signup, no view counter, no command palette. The portfolio has a command palette and it earned its keep there. A reading site is mostly absence — adding a feature has to clear a higher bar.