Skip to content
Back to projects

Web Development

Bilingual CMS & Portfolio Platform

The custom CMS behind this site: an admin dashboard with bilingual content, scheduled publishing, and media on Cloudflare R2, powering a fast public site with a WebGL hero, scroll-driven motion, and generated share images.

Bilingual CMS & Portfolio Platform

Overview

This site runs on a content management system I designed and built myself, from the database to the interface. I write each project, article, and profile section once per language in an admin dashboard, and the public site renders it with motion, 3D, and SEO built in.

The challenge

Portfolio builders either look generic or make bilingual content awkward. I wanted full control over the design, English and Indonesian versions of everything without duplicated work, and a site that stays fast and accessible even with heavy visuals. It also had to be easy to update, because a portfolio that is painful to edit goes stale.

What I built

Admin dashboard

  • Content for projects, articles, profile, work history, services, testimonials, and more, with English and Indonesian side by side.

  • A rich text editor with headings, lists, tables, images, YouTube embeds, and Mermaid diagrams with a live preview, used for the architecture diagrams on this page.

  • Draft, scheduled, and published states, with a daily job that publishes scheduled content.

  • Media uploads to Cloudflare R2, with image cropping.

  • SEO fields for titles, descriptions, and social cards, behind a protected sign-in.

Bilingual content with fallbacks

  • Every entry has an English and an Indonesian version. When the Indonesian one is missing, the English version is shown with a clear label and kept out of search indexes, so nothing is duplicated.

  • Localized URLs and hreflang links tell search engines which version to show.

Public site

  • A WebGL 3D signature in the hero, scroll-driven animations with GSAP, and smooth scrolling.

  • Quality tiers that adapt effects to the device: lighter versions for phones, still artwork without WebGL, and calm pages for reduced motion.

  • Generated social share images for pages and articles.

Speed, SEO, and AI search

  • Pages served from Vercel’s CDN with background revalidation, so content updates within a minute without slowing visitors down.

  • Responsive AVIF and WebP images, and heavy scripts such as the 3D scene and diagrams loaded only when needed.

  • Structured data, a sitemap, RSS feeds, and an llms.txt summary for AI search tools.

Architecture overview

A high-level view. One Astro application serves both the public site and the admin dashboard on Vercel. Content lives in PostgreSQL and media in Cloudflare R2.

flowchart LR
  accTitle: CMS and portfolio architecture
  accDescr: Visitors are served from Vercel’s CDN. One Astro app renders the public site and the admin dashboard, with content in PostgreSQL and media in Cloudflare R2. A daily job publishes scheduled content.
  visitor([Visitor]) --> cdn["Vercel CDN<br/>cached pages"]
  editor([Editor]) --> adminApp
  cron["Daily publishing job"] --> db
  subgraph astro["Astro app on Vercel"]
    site["Public site<br/>EN and ID pages, share images, feeds"]
    adminApp["Admin dashboard<br/>sign-in required"]
  end
  cdn --> site
  site & adminApp --> db[("PostgreSQL<br/>content")]
  adminApp --> r2[("Cloudflare R2<br/>media")]
  visitor -. images .-> r2

Content model

Every piece of content follows the same simple model, which is what makes two languages manageable:

classDiagram
  accTitle: Content model
  accDescr: An entry has one translation per language. Translations use media, and entries are grouped by categories and tags.
  class Entry {
    +type
  }
  class Translation {
    +language
    +status
    +title
    +slug
    +body
  }
  class Media {
    +file
    +altText
  }
  class Taxonomy {
    +kind
    +name
  }
  Entry "1" --> "1..2" Translation : has
  Entry "*" --> "*" Taxonomy : grouped by
  Translation --> Media : uses

Publishing lifecycle

stateDiagram-v2
  accTitle: Publishing lifecycle
  accDescr: Every entry starts as a draft. It can be published right away or scheduled, and the daily job publishes scheduled entries.
  [*] --> Draft
  Draft --> Scheduled: Set a publish date
  Draft --> Published: Publish now
  Scheduled --> Published: Daily job publishes it
  Scheduled --> Draft: Unschedule
  Published --> Draft: Unpublish

How a page reaches a visitor

sequenceDiagram
  accTitle: How a page reaches a visitor
  accDescr: The CDN answers from its cache. When its copy is older than a minute, it still answers instantly and renders a fresh page in the background.
  actor Visitor as Visitor
  participant CDN as Vercel CDN
  participant App as Astro app
  participant DB as PostgreSQL
  Visitor->>CDN: Requests a page
  alt Cached copy is fresh
    CDN-->>Visitor: Cached page
  else Copy is older than a minute
    CDN-->>Visitor: Cached page, instantly
    CDN->>App: Renders a fresh copy in the background
    App->>DB: Reads published content
    DB-->>App: Content
    App-->>CDN: Fresh page for the next visitor
  end

Tech stack

Astro, TypeScript, PostgreSQL with Drizzle ORM, Better Auth, Plate, Tailwind CSS, Cloudflare R2, and Vercel, with GSAP, Lenis, Three.js, and Mermaid on the public side.

Result

The platform runs raflimaulana.dev in English and Indonesian, and I publish new work without touching code. In Lighthouse, its pages score 100 for accessibility, best practices, and SEO.

Next project

Bionesia

Front-End Developer · 2026