Skip to content

Releases: SumitGA/SumitGautam.tech

v1.7.2

Choose a tag to compare

@SumitGA SumitGA released this 15 Sep 06:07

The sitemap had not changed since the v1.7.1 deploy on 7 September. Six days
later the live file still listed 4 of 6 posts and 12 of 17 tags — the two
newest HazShield parts and five tag pages were absent, so search engines were
being told with complete confidence that pages which exist do not.

app/sitemap.js exported revalidate = 3600 and the build manifest recorded
it, but neither the hourly window nor an explicit revalidatePath("/sitemap.xml")
moved it; the revalidate endpoint returned revalidated: true while the file
stayed frozen. Next documents sitemap.js as "a special Route Handler that is
cached by default", and that default outranks the revalidate window.

The sitemap is now an ordinary Route Handler at app/sitemap.xml/route.js —
the shape blog/rss.xml/route.js has used all along, which stayed current
throughout the same period. The URL, robots.txt and lib/site.js are
unchanged; nothing else knows the difference.

Verified against a local production build: 36 URLs (was 29), all 6 posts, all
17 tags, valid XML with no duplicates, cached between requests, and the
lastmod moves when the admin's save pushes a revalidate — which is precisely
what the old file would not do.

Known issues

  • Verified on npm run start, not behind Vercel's CDN. This repository has
    shown that difference to matter before. Confirm against the live file after
    promoting.
  • A release is not live until it is promoted by hand in Vercel; pushes land as
    Preview deployments.
  • Part 4 ("A window into the plant") still has no tags, so it contributes no
    tag pages and no internal links.

v1.7.1

Choose a tag to compare

@SumitGA SumitGA released this 07 Sep 07:51

Accessibility. PageSpeed Insights scored the site 99 desktop / 90 mobile
on performance and 100 on SEO, but 79 on accessibility. This closes the
gap between them.

Seven social links wrapped only <i class="fab fa-*"> and an empty
<span>. To a screen reader that is seven identical "link"s with no
destination. The certificate links on /education were the same shape —
an image and an overlay div, no text.

All now carry aria-label, with the icon and the decorative span marked
aria-hidden so the glyph is not announced alongside the name.

The theme toggle's only content is an SVG, so it announced as "button".
Its label names the action — "Switch to dark theme" — rather than the
current state, since the action is what a user needs in order to decide
whether to press it.

The home page carried four h1s: the greeting, the skills header, and two
skill card titles. /experience and /education jumped from h1 straight to
h3.

Section headings are h2 now, card titles h3, and each page keeps exactly
one h1.

No CSS anywhere selects on a heading tag — every heading is styled by
class — which is what made this safe to change without touching the
design. Worth checking before moving a heading level in future.

Ten pages checked mechanically against rendered HTML, not by eye: one h1
per page, zero skipped levels, zero links or buttons without an
accessible name.

Screenshots of the three pages whose headings moved confirm the design is
unchanged.

The 79 also reflects whatever Lighthouse measured on the live build; the
score should be re-run after this deploys to confirm the size of the
improvement rather than assuming it.

  • Both posts share a cover image, so the blog index shows the same
    artwork twice.
  • Production still requires a manual "Promote to Production" in Vercel.
  • Six analytics rows point at deleted test posts.

v1.7.0

Choose a tag to compare

@SumitGA SumitGA released this 06 Sep 15:12

Three things that were quietly working against the blog.

The nav was the only way in, which asks a visitor to go looking for
something they have no reason to believe exists. The three most recent
posts now sit between Skills and the footer, with title, date, reading
time and excerpt.

It renders nothing when there are no posts — the same rule the nav link
follows, so an empty blog never advertises itself.

Every solid accent fill behind white text measured 3.93:1, under the
4.5:1 AA requires for a button label. accentSolid (#E80F4F, 4.55:1) was
added in v1.3.0 but only the new author card used it.

Eight call sites are now switched: the greeting, projects and contact
CTAs, the case-study live link, the contact form's submit button, and
the blog tag chips. accentBright stays for non-text uses, where the
threshold is 3:1 rather than 4.5:1.

It said "I don't blog frequently but when I do something awesome, I do
try to document it... I write on Twitter", with a button reading "My
Twitter Profile" — while two deep technical posts sat on this domain.
Actively false, and it sent the most interested visitors away.

The copy is CMS content and has been updated. The button label was not:
it was hardcoded, so it now follows wherever blog_link points, and uses
a Link for an internal path so navigation stays client-side. Change the
copy in the admin under Contact; the label lives in
src/views/contact/ContactComponent.js.

The social icon row still links Twitter, which is correct.

  • Both posts share a cover image, so the blog index shows the same
    artwork twice.
  • Production still requires a manual "Promote to Production" in Vercel.
  • Callout blocks (> [!NOTE]) are not rendered.
  • src/App.js, src/index.js, src/index.css and src/containers/Main.js are
    dead CRA files nothing imports.

v1.6.0

Choose a tag to compare

@SumitGA SumitGA released this 06 Sep 11:40

The site assistant now knows about the writing.

Before this it answered from the resume and case studies only, so
"has he run Rust under load?" — the subject of four thousand words and a
deliberate database-murder load test — got a resume bullet.

The system prompt is rebuilt every ten minutes and sent with EVERY
message. The module-scope cache saves database queries, not tokens.
Anything added is therefore paid for on every turn, and would grow
without limit as posts accumulate: fine for a resume, which is bounded;
wrong for a blog, which is unbounded by design.

Every post contributes title, excerpt and section headings. Only the
newest three contribute their full body.

full body ~2,337 tokens/post
title + excerpt + headings ~145 tokens/post

A 16:1 ratio, and that ratio is the entire design. Headings are the
load-bearing part — "Act III: The Execution", "A Database Designed to
Survive the Firehose" describe the territory precisely enough for the
model to answer specifically and cite the path.

Projected total prompt:

posts | this design | every post in full
2 | 11,362 | 11,073
10 | 14,855 | 29,765
100 | 27,860 | 240,050

Two honest notes, both in the doc. This is linear with a smaller slope,
not flat. And at two posts it is marginally WORSE than sending
everything, because both are already full and now pay for headings too —
it starts paying off around four posts.

With full text, asked whether Sumit had tested Rust under load: correct
figures, correct slug, nothing invented.

With full text disabled, asked for an exact p99 it had not been shown:
it declined to guess, pointed at the post and offered the contact form.
That is the failure mode that matters, since summarised posts are the
ones it could confabulate about.

Written to be read cold. Measurements, the three options considered and
why retrieval was not built yet, the projection, the trigger for
switching to it, the two cheaper optimisations that come first (the
projects section is 70% of the base prompt), and the failure modes.

Read it before changing FULL_POSTS_IN_PROMPT.

  • The active tag chip on the blog index measures 3.93:1, below AA.
  • The contact page still says "I write on Twitter" and links there —
    a CMS edit, not code.
  • The home page links to no post.
  • Production still requires a manual "Promote to Production" in Vercel.

v1.5.0

Choose a tag to compare

@SumitGA SumitGA released this 06 Sep 11:21

Admin-only release. The public site is unchanged, so promoting the admin
project is what matters here.

The cover field could only upload. Using one picture in two posts — a
series cover, a diagram that belongs in both — meant a second copy:
storage spent twice on a finite plan, and two public_ids for one image,
so a later cleanup could only ever find half of it.

"Choose existing" now lists what is already in the account, for covers
and for body images. Picked images are still written to post_images: the
upsert is keyed on (post_id, public_id), and shared ownership is exactly
what stops deleting one post removing an asset another post still uses.

Listing goes through the Admin API, since there is no public way to
enumerate an account's assets — correctly, as that would expose
everything ever uploaded. Gated on a real Supabase session, the same as
signing and deleting; verified returning 401 with no token and with a
bad one. Only public_id, dimensions, size and date reach the browser.

Both directions were manual. Retyping is where canonical_url gets
forgotten, and a cross-post without it hands the ranking to the other
site — the opposite of the reason for publishing here.

Import parses the frontmatter and fills title, excerpt, tags, series and
cover. A cover hosted outside this Cloudinary account is reported rather
than silently dropped, because a cover that quietly vanished would be
found much later, on the live site.

"Copy for dev.to" emits the block with canonical_url already pointing
here, plus the body. It sets published: false so a cross-post is
reviewed there before going live, strips hyphens from tags and caps them
at four, because dev.to rejects both.

The parser is hand-written rather than a YAML dependency: this
frontmatter is flat scalars and one comma list. 23 tests cover it,
including the exact dev.to header Part 2 was pasted from — commented
keys ignored, empty values not written, quotes stripped, round trip.

URLs for picked images are encodeURI'd. A public_id may contain a space
— this account already has "My Brand/watermark" — and markdown ends a
link target at the first space, so an unencoded URL silently produces a
broken image. Uploads were never affected: Cloudinary returns an encoded
URL.

The picker UI itself was not clicked through, since that needs admin
credentials. The route, the mapping and the parser are all verified;
the modal is not.

  • The active tag chip on the blog index measures 3.93:1, below AA.
  • Both posts share a cover image, so the index shows the same artwork
    twice.
  • Production still requires a manual "Promote to Production" in Vercel.
  • Callout blocks (> [!NOTE]) are not rendered.

v1.4.1

Choose a tag to compare

@SumitGA SumitGA released this 06 Sep 11:08

Blog images no longer shift the layout or all load at once.

Every body figure rendered as a bare . No width or height, so
each one pushed the text down as it arrived. No loading hint, so all
four figures on a post loaded eagerly, competing with the image the
reader could actually see. Both are Core Web Vitals metrics Google ranks
on, and both were free to fix.

A rehype plugin adds, to every body image:

  • loading="lazy" and decoding="async"
  • srcset at 480/720/1080/1440 with sizes for the 720px prose column
  • intrinsic width/height so the space is reserved before bytes arrive

It works on the rendered tree rather than changing how images are
authored. The markdown keeps full portable URLs — one that renders in
GitHub was a deliberate choice, and rewriting the source to carry
dimensions would undo it. Images not on our own Cloudinary get the
loading hints only, since we cannot resize what we do not serve.

Dimensions come from Cloudinary's fl_getinfo, a public delivery URL
rather than the Admin API, so no credentials are involved. It runs at
build and revalidate time only, never in a visitor's request, and is
memoised per process. On failure it returns null: the image still
renders, it just does not reserve space.

Eager, with fetchPriority="high". It is the LCP element on every post,
so lazy-loading the one image that has to arrive first would be exactly
backwards.

.post-cover sets width:100% and had no height:auto. The moment a tag
carries intrinsic width/height, the attribute height wins over the
scaled width and the image stretches. Anything given dimensions needs
height:auto alongside.

  • The active tag chip on the blog index uses accentBright behind white
    text and measures 3.93:1, below AA.
  • Both posts share a cover image, so the index shows the same artwork
    twice.
  • Production still requires a manual "Promote to Production" in Vercel.
  • Callout blocks (> [!NOTE]) are not rendered.
  • The admin blog editor cannot reference an existing Cloudinary asset,
    only upload a new one, so re-using an image duplicates it.

v1.4.0

Choose a tag to compare

@SumitGA SumitGA released this 06 Sep 10:53

Related content. A case study and the posts that go deep on the same work
now link to each other, and a series holds together regardless of what
gets published between its parts.

Run supabase/blog_series.sql in the Supabase SQL Editor BEFORE
deploying. It adds three columns to posts; without them PostgREST
rejects the whole select and the blog index renders empty rather than
degrading. Idempotent, safe to re-run.

series + series_order decide reading order. Previous/next stays
chronological on purpose — it answers "what else is there", which is a
different question from "where am I in this set". Before this, the only
thing linking Part 1 to Part 2 was publish date, so anything published
between them would have stranded the series.

The database enforces the invariant rather than the application
remembering to:

  • a series with no position is rejected (posts_series_needs_order)
  • two parts claiming one position is rejected (posts_series_order_key)

Both verified against the live database, not just locally.

Unpublished parts are absent rather than greyed out. RLS already hides
them, and a link a reader cannot follow is worse than not knowing the
part is coming.

A post can name a project. The post shows what it is about; the case
study grows a "build log" listing the posts. That direction did not
exist at all.

No foreign key, deliberately: projects.slug is unique only through a
partial index — blank slugs are allowed for projects without a case
study — and Postgres cannot back a foreign key with one. The slug
resolves at render time through getProjectBySlug(), which returns null
unless the project actually has a case study page, so a stale slug
renders no link rather than a dead one.

Series name with a part number, disabled until a series is named, and a
project dropdown listing only projects that have a case study page — a
link that goes nowhere should be impossible to create, not merely
discouraged.

  • The active tag chip on the blog index uses accentBright behind white
    text and measures 3.93:1, below AA. Predates the accentSolid token.
  • Body images carry no width/height or loading="lazy", so every figure
    shifts the layout as it loads and all of them load eagerly.
  • Both posts share a cover image, so the index shows the same artwork
    twice.
  • Production still requires a manual "Promote to Production" in Vercel.
  • Callout blocks (> [!NOTE]) are not rendered.
  • The admin blog editor cannot reference an existing Cloudinary asset,
    only upload a new one, so re-using an image duplicates it.

v1.3.0

Choose a tag to compare

@SumitGA SumitGA released this 06 Sep 10:26

Turns a finished post from a dead end into a starting point, and makes
the RSS feed possible to subscribe to.

Every post now ends with who wrote it and two things to do: "Get in
touch" and "Subscribe via RSS". Before this, the article stopped and the
next thing was previous/next links — the most engaged visitor the site
gets was given no identity to attach the writing to and no next step.

Two actions, deliberately. One converts, one retains. More would dilute
both.

Name and bio come from the CMS greeting, so editing it in the admin
updates the card, matching the rule the footer already follows.

Both actions are tracked as conversions (post_contact_click,
post_subscribe_click), so the analytics dashboard shows whether the
card actually works. Separate event names rather than one event with
metadata, so they appear as distinct rows with no schema change.

/blog/rss.xml has existed since v1.2.0 and was linked from nowhere — no
visible link, and no in , which is how feed
readers discover a feed at all. Subscribing required guessing the URL.

Now emitted on the blog index, every post and every tag page, plus a
visible link on the index.

It lives on the blog routes rather than the root layout on purpose: a
child that sets alternates replaces the parent's object wholesale, and
every page here sets its own canonical, so putting it in the layout would
have dropped it from precisely the pages that need it. Verified against a
production build that all three route shapes emit both.

Adds accentSolid (#E80F4F). White on accentBright (#FC1056) measures
3.93:1 — fine for large text, below the 4.5:1 AA requires for a button
label. The new colour is the same red 8% darker, at 4.55:1.

  • The active tag chip on the blog index still uses accentBright behind
    white text and so still measures 3.93:1. It predates this token.
  • Both posts share a cover image, so the index shows the same artwork
    twice.
  • Production still requires a manual "Promote to Production" in Vercel.
  • Callout blocks (> [!NOTE]) are not rendered.
  • The admin blog editor cannot reference an existing Cloudinary asset,
    only upload a new one, so re-using an image duplicates it.

v1.2.2

Choose a tag to compare

@SumitGA SumitGA released this 06 Sep 05:50

Fixes the sitemap and the RSS feed going stale after a publish.

Publishing the first blog post exposed it. The post was live, rendered
correctly and served a valid canonical — but /sitemap.xml did not list
it, and would not have for up to an hour.

revalidatePath("/", "layout") invalidates the root layout, every nested
layout and every page beneath them. The sitemap and the feed are route
handlers, not pages, so they are outside that tree. Each carried its own
revalidate = 3600 and kept serving build-time output until it expired.

The failure is quiet: the post itself is fine, so nothing on the site
suggests the sitemap is a post behind. The hour it stays wrong is the
hour a newly published post most needs a crawler to find it.

/api/revalidate now invalidates /sitemap.xml and /blog/rss.xml
explicitly, alongside the existing layout call.

Verified against a real production build rather than assumed. The
sitemap stamps lastmod at generation time, which makes regeneration
observable: it held steady across repeated fetches, then advanced
immediately after a revalidate call.

  • Production still requires a manual "Promote to Production" in Vercel;
    the stale main branch remains the likely cause.
  • Callout blocks (> [!NOTE]) are not rendered.
  • The admin blog editor cannot reference a Cloudinary asset that already
    exists — only upload a new one, so re-using an image duplicates it.

v1.2.1

Choose a tag to compare

@SumitGA SumitGA released this 06 Sep 04:17

Housekeeping around the blog and analytics.

Fixed

  • Running the site locally was recording analytics events into the live
    database, so the dashboard counted visits that never happened. Development
    no longer writes events
  • The Blog link appeared in the navigation before anything was published,
    leading visitors to an empty page. It now appears only once there is a post

Notes

  • A production build served from localhost still records events, which is what
    you want when checking the pipeline works
  • Drafts and scheduled posts do not bring the navigation link back, since the
    check uses the same visibility rules as the public site