Introducing the new shramko.dev

Published on

Last updated on

12 min read--- views

Screenshot of the redesigned shramko.dev homepage showing dark theme with featured blog posts

What started as a two-month rewrite in the summer of 2022 has turned into a nearly four-year engineering project — 940+ commits, 33 blog posts, 41 code snippets, and a full-stack architecture that I keep pushing forward with every new version of the JavaScript ecosystem.

Here is an overview of what powers shramko.dev today.

Overview

The cloc stats, counting only git-tracked files:

cloc --vcs=git . ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- TypeScript 130 1050 39 8273 Markdown 81 2311 9 7858 YAML 3 2057 0 7847 JavaScript 8 35 10 490 Text 4 57 0 300 JSON 8 0 0 267 CSS 2 47 4 233 XML 2 0 0 118 Prisma Schema 1 5 0 23 SQL 1 9 6 21 SVG 1 0 0 17 INI 1 2 0 11 Bourne Shell 3 3 0 9 ------------------------------------------------------------------------------- SUM: 245 5576 68 25467 -------------------------------------------------------------------------------

Update: Lines of code on

TypeScript is the dominant application language at ~8 300 lines. Markdown, which represents every blog post and snippet, has now nearly caught up at ~7 900 — a ratio I consider healthy for a content-first site.

The first commit was in 3 Jul 2022.

Key features

  • Dark and Light mode via next-themes with class-based Tailwind switching
  • Security headers including a strict Content Security Policy, HSTS with preload, and a locked-down Permissions-Policy
  • Featured posts with frontmatter meta parsing, reading time, and auto-generated table of contents
  • Custom 404 error page
  • Fully responsive styling with Tailwind CSS 4
  • Dynamic Open Graph images generated at the edge with @vercel/og
  • Accessibility targeting WCAG 2.1 and WCAG 2.2
  • Six REST API endpoints backed by PostgreSQL — views, reactions, waitlist, dashboard, GitHub stats, and OG image generation
  • Real-time client updates with SWR
  • RSS feed at /feed.xml with proper cache headers and CDATA escaping
  • Schema.org structured data — BlogPosting, TechArticle, BreadcrumbList, FAQPage, and WebSite schemas
  • Post reactions (heart, beer, trophy) with optimistic UI via a custom useReducer
  • Lighthouse CI on every deploy preview with enforced performance budgets (LCP < 2.5 s, TBT < 1 s, CLS < 0.1)
  • Auto-generated sitemap with depth-based priority and frontmatter-driven lastmod dates

💻 Technologies

The core stack:

The services running behind the scenes:

  • Vercel: Hosting, preview deploys, edge functions, and analytics
  • GitHub Actions: CI pipeline — lint, test, and Lighthouse workflows
  • Sentry: Error tracking and performance monitoring (10% trace sampling) via Next.js instrumentation hooks
  • Vercel Analytics + Speed Insights: Real-user metrics

🍭 The new look

Here is how the original design looked before the rewrite.

A plain HTML and CSS page. No JS, no frameworks 😂.

Previous version of shramko.dev hosted on GitHub Pages showing resume layout with experience and tech stack sections

🚀 Deploy

Every commit triggers a Vercel build and creates either a Production or Preview environment. Three GitHub Actions workflows run in parallel:

I have a post about ESLint with TypeScript

  • Lintoxlint for fast, zero-config linting across the entire codebase
  • Test — Jest 30 runs the full suite in CI mode
  • Lighthouse CI — Three runs per URL (/, /blog, /about) against the preview deploy, with results posted as a sticky PR comment

All three must pass before the site ships to production.

Vercel deployment overview showing 100 scores for Virtual Experience, First Contentful Paint, Largest Contentful Paint, Cumulative Layout Shift, and Total Blocking Time

MDX Compilation

Every blog post and snippet is an MDX file compiled at build time through next-mdx-remote. The compilation pipeline threads content through remark and rehype, turning raw Markdown into richly interactive React:

import { serialize } from ‘next-mdx-remote/serialize’; import remarkGfm from ‘remark-gfm’; import rehypeSlug from ‘rehype-slug’; import rehypeCodeTitles from ‘rehype-code-titles’; import rehypeAutolinkHeadings from ‘rehype-autolink-headings’; import rehypeShiki from ‘@shikijs/rehype’; export const compileMDX = async (content: string) => serialize(content, { mdxOptions: { remarkPlugins: [remarkGfm], rehypePlugins: [ rehypeSlug, rehypeCodeTitles, [ rehypeShiki, { themes: { light: ‘github-light’, dark: ‘github-dark’ }, defaultColor: false } ], [ rehypeAutolinkHeadings, { properties: { className: [‘anchor’] } } ] ], format: ‘mdx’ } });

A separate extractHeadingsFromMarkdown function parses heading structure from the raw Markdown to build the auto-generated table of contents — no extra runtime dependency required.

import { MDXComponents } from ‘@/components/mdx-components’; <div className="w-full mt-4 prose dark:prose-dark max-w-none"> <MDXRemote {...content} components={MDXComponents} /> </div>

Monitoring with Sentry

Sentry is wired in through two layers. The server side uses Next.js instrumentation hooks to capture errors and filter out network noise like ECONNRESET and ETIMEDOUT. Events without a stacktrace are dropped before they leave the server — a simple beforeSend guard that keeps the dashboard clean:

import * as Sentry from ‘@sentry/nextjs’; export function register() { Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, environment: process.env.NODE_ENV, release: process.env.APP_RELEASE_VERSION, sendDefaultPii: true, tracesSampleRate: 0.1, ignoreErrors: [ ECONNRESET’, ECONNREFUSED’, ETIMEDOUT’, UND_ERR_CONNECT_TIMEOUT’, ], beforeSend(event) { if (!event.exception?.values?.some((e) => e.stacktrace)) { return null; } return event; }, }); } export const onRequestError = Sentry.captureRequestError;

The client-side instrumentation filters out a different class of noise — browser extensions, Safari WebKit quirks, ResizeObserver loops, and Google Translate proxy URLs — the kind of false positives that would otherwise drown the real signal.

The Next.js config uses a "dispatch table" pattern to select the right Sentry build options per environment, wrapping the config with withSentryConfig only in production.

Database and Prisma

Prisma 7 with the @prisma/adapter-pg driver powers three models — page view tracking, post reactions, and a waitlist:

generator client { provider = "prisma-client" output = "../generated" } datasource db { provider = "postgresql" } model views { slug String @id @db.VarChar(128) count BigInt @default(1) } model reactions { id Int @id @default(autoincrement()) slug String @db.VarChar(128) type String @db.VarChar(32) count BigInt @default(0) @@unique([slug, type]) } model waitlist { id Int @id @default(autoincrement()) email String @unique @db.VarChar(255) createdAt DateTime @default(now()) @map("created_at") }

The views model uses upsert — if the slug exists, increment; otherwise, create. The reactions model supports three types (heart, beer, trophy) with a composite unique constraint on [slug, type], and the API handles increments inside a Prisma transaction.

Querying Data

const views = await prisma.views.upsert({ where: { slug }, create: { slug, }, update: { count: { increment: 1, }, }, });

Testing

Tests run on every push and every pull request through GitHub Actions. The suite has grown from 9 test suites and 37 tests in 2022 to 23 suites and 183 tests today, covering API routes, components, utilities, content parsing, schema generation, and page rendering. The most recent push closed several long-standing coverage gaps in lib/posts/api, the dashboard endpoint, the view counter, the blog list page, and the blog-post preview — focusing on business logic rather than render output.

Current coverage on :

------------------------------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ------------------------------|---------|----------|---------|---------|------------------- All files | 97.27 | 91.26 | 91.66 | 97.27 | components/blog-post-preview | 100 | 66.66 | 100 | 100 | blog-post-preview.tsx | 100 | 100 | 100 | 100 | index.ts | 100 | 40 | 100 | 100 | 1 components/categories | 100 | 57.14 | 100 | 100 | categories.tsx | 100 | 100 | 100 | 100 | index.ts | 100 | 40 | 100 | 100 | 1 components/footer | 100 | 100 | 100 | 100 | get-copyright.ts | 100 | 100 | 100 | 100 | components/mobile-menu | 100 | 100 | 100 | 100 | icons.tsx | 100 | 100 | 100 | 100 | mobile-menu.tsx | 100 | 100 | 100 | 100 | components/no-results | 92.3 | 50 | 100 | 92.3 | index.ts | 100 | 100 | 100 | 100 | no-results.tsx | 92 | 50 | 100 | 92 | 19-20 components/search-input | 100 | 100 | 100 | 100 | index.ts | 100 | 100 | 100 | 100 | search-input.tsx | 100 | 100 | 100 | 100 | components/share-button | 100 | 100 | 100 | 100 | share-button.tsx | 100 | 100 | 100 | 100 | components/tag | 81.81 | 33.33 | 100 | 81.81 | index.ts | 100 | 100 | 100 | 100 | tag.tsx | 81.25 | 33.33 | 100 | 81.25 | 20-25 components/theme-changer | 90.24 | 53.33 | 100 | 90.24 | index.ts | 100 | 40 | 100 | 100 | 1 theme-changer.tsx | 90 | 60 | 100 | 90 | 32-33,35-36 components/view-counter | 100 | 85.71 | 100 | 100 | index.ts | 100 | 40 | 100 | 100 | 1 view-counter.tsx | 100 | 100 | 100 | 100 | components/year-separator | 100 | 100 | 100 | 100 | index.ts | 100 | 100 | 100 | 100 | year-separator.tsx | 100 | 100 | 100 | 100 | lib | 100 | 98.18 | 80 | 100 | constants.ts | 100 | 100 | 100 | 100 | feed.ts | 100 | 100 | 100 | 100 | fetcher.ts | 100 | 100 | 100 | 100 | github.ts | 100 | 100 | 100 | 100 | routes.ts | 100 | 100 | 57.14 | 100 | schema.ts | 100 | 94.73 | 100 | 100 | 71 types.ts | 100 | 100 | 100 | 100 | utils.ts | 100 | 100 | 100 | 100 | lib/posts | 100 | 100 | 100 | 100 | api.ts | 100 | 100 | 100 | 100 | utils.ts | 100 | 100 | 100 | 100 | lib/scripts | 49.33 | 100 | 50 | 49.33 | compiler.ts | 49.33 | 100 | 50 | 49.33 | 22-59 pages | 100 | 100 | 100 | 100 | 404.tsx | 100 | 100 | 100 | 100 | pages/api | 100 | 100 | 100 | 100 | dashboard.ts | 100 | 100 | 100 | 100 | github.ts | 100 | 100 | 100 | 100 | waitlist.ts | 100 | 100 | 100 | 100 | pages/api/reactions | 100 | 93.33 | 100 | 100 | [slug].ts | 100 | 93.33 | 100 | 100 | 88 pages/api/views | 100 | 100 | 100 | 100 | [slug].ts | 100 | 100 | 100 | 100 | pages/blog | 100 | 95.65 | 100 | 100 | index.tsx | 100 | 95.65 | 100 | 100 | 108 ------------------------------|---------|----------|---------|---------|------------------- Test Suites: 23 passed, 23 total Tests: 183 passed, 183 total Snapshots: 0 total Time: 1.404 s Ran all test suites.

Sometimes I use Browserstack for cross-browser testing.

Next.js

The site runs on Next.js 16 with React 19 — still on the Pages Router, which remains a first-class citizen in the framework. A few reasons Next.js continues to be the right foundation:

  • Static Generation + Server Rendering: Every blog post and snippet is pre-rendered at build time via getStaticProps and getStaticPaths, while API routes handle dynamic data
  • One-click deploys: Deep Vercel integration means every push generates a preview URL
  • SEO built in: Auto-generated sitemaps, structured data, and dynamic OG images with zero extra infrastructure
  • Ecosystem depth: The React and Node.js ecosystems provide a package for nearly every need, from MDX compilation to Prisma database access
  • Performance defaults: SWC compilation, automatic code splitting, image optimization with configurable quality levels
Two-button meme: a sweating man choosing between 'Learn yet another framework' and 'Lose my job'

I wrote articles about another framework — Astro here and here 😆.

Since launching, I have also run an AI-powered SEO audit across the entire blog to improve discoverability.

Code quality

The project enforces quality at multiple checkpoints:

  • oxlint — a fast, Rust-based linter that replaced ESLint with zero-config setup and 136 rules
  • Prettier for consistent formatting (single quotes, trailing commas, 80-char width)
  • Conventional Commits enforced by commitlint via a commit-msg git hook
  • Pre-push hook that runs the full linter before code leaves the local machine
  • Browserslist set to baseline widely available, which targets browsers with broad cross-engine support

Acknowledgements

Conclusion

What I have learned building this site over four years would fill a book. The stack has evolved from plain HTML to Next.js 16 with React 19. The database grew from nothing to three Prisma models. The test suite went from 37 tests to 183. And the content — 33 posts, 41 snippets — keeps growing, partly thanks to the resources I recommend to every developer. 🤓

The project is still developing. You can see open features or suggest new ones here.

Share it: