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.
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@themeblock. 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.
Related posts and prev/next
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, sitemap, search
/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.jsonand 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.