Introducing the new shramko.dev
Published on
Last updated on
12 min read • --- views
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.xmlwith 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
lastmoddates
💻 Technologies
The core stack:
- React 19: The UI layer
- Next.js 16: Framework for hybrid static and server rendering (Pages Router)
- TypeScript 6: Strict mode, path aliases, ES2022 target
- Prisma 7: Type-safe ORM with the
@prisma/adapter-pgdriver - SWR 2: Stale-while-revalidate data fetching
- Jest 30 + Testing Library: Unit and component testing with V8 coverage
- Tailwind CSS 4: Utility-first styling with
@tailwindcss/postcssand the typography plugin - next-mdx-remote 6: MDX compilation with remark/rehype pipelines
- gray-matter: YAML frontmatter parsing
- PostgreSQL: The database behind views, reactions, and the waitlist
- pnpm 10: Package manager — I wrote a post about migrating to it
- Node.js 24: Runtime, pinned via
.nvmrc - lucide-react: Icon library, tree-shaken via
optimizePackageImports
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 😂.
🚀 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
- Lint — oxlint 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.
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
withSentryConfigonly 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
getStaticPropsandgetStaticPaths, 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
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-msggit 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
- The design was inspired by Lee Robinson
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: