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.

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 .-> r2Content 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 : usesPublishing 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: UnpublishHow 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
endTech 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
