← Work
Product

Sitnice Bitnice

A full-stack bilingual (Croatian / English) recipe and editorial platform, built solo end-to-end: a legacy WordPress site rebuilt on Payload CMS 3 + Next.js 15, with an automated WordPress-to-headless migration pipeline, anonymous IP-hashed ratings, three-tier RBAC, and a multi-layer caching strategy on Vercel and Supabase.

Role
Sole full-stack developer
Timeframe
2026 – Present
Stack
Next.js 15 · React 19 · Payload CMS 3 · TypeScript · PostgreSQL · Supabase · Drizzle ORM · Tailwind CSS · shadcn/ui · next-intl · Lexical · Resend · Vercel · pnpm
sitnice bitnice hero
A production-grade, bilingual (Croatian / English) recipe blog and editorial platform built on Payload CMS 3 + Next.js 15, migrated from a legacy WordPress site and deployed on Vercel with Supabase infrastructure. Designed, built, and operated end-to-end as the sole full-stack developer — architecture, backend, frontend, CMS configuration, content migration, deployment, and production operations.

Key Features Built

Full-stack bilingual CMS platform

  • Architected a bilingual (Croatian / English) recipe and editorial platform on Payload CMS 3 + Next.js 15, with localized content across all collections — titles, descriptions, ingredients, steps, slugs, rich text, and SEO metadata (default locale Croatian, English fallback).
  • Implemented locale-aware routing with culturally appropriate URL paths — /recepti (recipes) and /objave (posts) — using next-intl with localePrefix: 'never' (state-based locale rather than URL-prefixed) plus permanent redirects for legacy English paths.

WordPress-to-Payload migration pipeline

  • Built a complete WXR (WordPress eXtended RSS) parser and importer that extracts WPRM (WP Recipe Maker) recipe data from PHP-serialized post meta, with a JSON fallback.
  • Automated media migration: downloads images from WordPress and re-uploads them to Supabase S3, linking them to Payload documents — including a dedicated step-image scraper for per-instruction images.
  • Implemented dry-run mode and skip-by-slug idempotency for safe, repeatable migrations; content imports as drafts by default with flags for --publish, --skip-images, --author, and --cleanup.

Recipe content system with structured data

  • Designed a rich recipe schema — ingredients with a three-mode unit system (metric / imperial / general), step-by-step instructions with per-step images, cooking time, servings, and difficulty.
  • Built smart client-side ingredient scaling that adjusts quantities to the reader's selected serving size and auto-converts units across thresholds (ml↔l, g↔kg, tsp↔tbsp), with tap-to-check-off strikethrough — all with no server round-trip.
  • Implemented Schema.org Recipe JSON-LD with aggregateRating, recipeIngredient, and HowToStep instructions for search-engine rich results.

Anonymous star rating system

  • Built a custom rating API (POST /api/ratings) with SHA-256 IP hashing (salted with PAYLOAD_SECRET) to enforce anonymous, one-vote-per-IP-per-recipe (returns HTTP 409 on repeat votes).
  • An afterChange hook recalculates the recipe's averageRating and ratingCount on every new vote; page freshness is handled separately via on-demand ISR revalidation so ratings appear without a redeploy.

Role-based access control (RBAC)

  • Implemented a three-tier access model (admin / editor / user) with function-based Payload access policies.
  • Granular permissions: editors can create and update but not delete; admin-only collections are hidden from the sidebar for non-admins; the public ratings endpoint is open for creation but locked for updates.
  • Extended editor access to forms and form submissions, with dedicated editor dashboard cards, so non-admin contributors can manage collaboration inquiries.

Draft/publish workflow with live preview

  • Enabled autosaving drafts (2-second interval, 25 versions per document) across Recipes, Posts, and Pages.
  • Configured live preview with three responsive breakpoints (mobile 375, tablet 768, desktop 1440) and a LivePreviewListener for real-time content refresh in the frontend.

Search & filtering with infinite scroll

  • Configured @payloadcms/plugin-search with custom beforeSync hooks that normalize slugs, SEO metadata, and categories into a flat search index.
  • Built recipe filtering with Next.js Server Actions (query, difficulty, category), rendered in an infinite-scroll grid.

Transactional email & self-service auth

  • Integrated Resend for transactional email, including a fully custom Croatian-language HTML password-reset template and self-service forgot-password token flow in the Users collection.
  • Extended the form builder's beforeEmail hook to parse comma-separated / JSON recipient lists for to/cc/bcc, and to build a fallback HTML table from raw submission data when the rendered body is empty — so no inquiry is ever lost.

CMS-managed forms & collaboration page

  • Built an idempotent setupCollab seeding endpoint (POST /api/setup-collab, admin-only) that programmatically creates a fully localized HR + EN collaboration form and page, carefully preserving Croatian-localized fields when seeding the English layout.
  • Integrated @payloadcms/plugin-form-builder with react-hook-form for client-side validation and Lexical rich-text confirmation messages.

Multi-layer caching strategy

  • Implemented ISR with on-demand revalidatePath from collection hooks, unstable_cache with tags for globals, and React.cache for request-level deduplication.
  • Built a Vercel Cron warmup endpoint (/api/warmup, every 8 minutes) that pings Payload/Postgres to keep serverless functions and the Supabase connection pool warm.

S3 media pipeline with filename sanitization

  • Configured Supabase Storage as an S3-compatible backend with public CDN URLs and Payload access control disabled for direct serving.
  • Built a sanitizeMediaFilename hook that transliterates Croatian diacritics (č→c, š→s, đ→dj), strips combining marks and non-ASCII characters, and normalizes spaces — since S3 rejects non-ASCII object keys.

Dark mode & print-optimized layout

  • Implemented theme switching via a data-theme attribute with localStorage persistence and system-preference detection, plus flash-prevention.
  • Built print CSS (A4 @page) that reformats recipe pages into a compact two-column ingredients + steps layout and hides all interactive chrome.

Architecture & Patterns

Co-located CMS + frontend

Payload CMS 3 runs inside the same Next.js process — no separate API server. The admin panel lives under /(payload)/admin, the auto-generated REST/GraphQL APIs under /(payload)/api, and the public frontend under /(frontend)/[locale].

Reusable block system

Content blocks (Archive, Banner, CallToAction, Code, Content, Form, Media, RelatedPosts, RelatedRecipes) follow a config.ts / Component.tsx / Component.client.tsx split, dispatched via a RenderBlocks mapper. Blocks can be embedded inline within Lexical rich text via BlocksFeature.

Custom admin UI

  • SlugComponent — a custom field with a lock/unlock toggle for auto-formatting vs. manual slugs.
  • BeforeLogin / AfterDashboard / AdminNav / AdminLogo / AdminIcon — branded admin customizations, including role-aware dashboard cards.
  • Bilingual admin labels via a custom t() helper that generates { en, hr } objects from message files.

Hierarchical categories

@payloadcms/plugin-nested-docs provides tree-structured categories with breadcrumbs, shared by posts and recipes.

Database connection management

The Postgres pool is tuned for Supabase's Supavisor pooler: max: 5 connections (sized for concurrent generateStaticParams at build time), 30s connect timeout, and a 20s idle timeout that closes connections before the pooler drops them.


Infrastructure & DevOps

Deployment

  • Vercel (Frankfurt fra1 region) with ISR, Server Actions, and Cron Jobs.
  • Supabase for PostgreSQL (Supavisor transaction pooler) and S3-compatible object storage.
  • Resend for transactional email delivery.

CI/CD

  • GitHub Actions — an automated weekly workflow that syncs from an upstream template repo, merges with an ours conflict strategy, and auto-opens a PR into main (peter-evans/create-pull-request).

Monitoring & reliability

  • /api/health — lightweight status endpoint for external monitoring.
  • /api/warmup — Vercel Cron (every 8 min) to prevent cold starts and pool timeouts.
  • On-demand ISR revalidation from CMS hooks keeps published content fresh without redeploys.

Project Ownership Highlights

  • Full project ownership — sole developer across architecture, backend, frontend, CMS configuration, content migration, deployment, and ongoing production operations.
  • Internationalization depth — end-to-end bilingual support spanning content, UI, admin panel, SEO, URL routing, and email templates.
  • Legacy system migration — planned and executed a WordPress-to-headless-CMS migration with automated data extraction from PHP-serialized meta, media re-hosting, and idempotent, resumable imports.
  • Production operations — connection-pool tuning, cold-start mitigation, CDN-optimized media delivery, and a layered caching strategy.
  • User-experience focus — live preview for editors, smart ingredient scaling for readers, responsive design, dark mode, and print-optimized recipes.