Personal portfolio + blog + dokumentasi, built with Express.js + EJS + Tailwind CSS.
webmd/
βββ content/ # Markdown sumber konten
β βββ blog/*.md # Artikel blog (10 file)
β βββ docs/*.md # Dokumentasi (2 file)
βββ data/ # Data konfigurasi JSON
β βββ config.json # Info situs & profil
β βββ navigation.json # Menu navbar
β βββ socials.json # Link sosial media
β βββ projects.json # Data proyek
βββ public/ # File statis (gambar, favicon)
βββ src/ # Kode backend
β βββ app.js # Entry point Express
β βββ controllers/
β β βββ pageController.js # Handler halaman (home, blog, docs)
β β βββ projectController.js # API projects
β βββ routes/
β β βββ index.js # Setup routing
β β βββ pages.js # Definisi route
β βββ middlewares/
β β βββ locals.js # Inject data ke semua view
β β βββ errorHandler.js # 404 & 500 handler
β βββ services/
β β βββ cache.js # Cache in-memory singleton
β β βββ contentLoader.js # Load data & markdown saat startup
β β βββ parser.js # Parse markdown β HTML via markdown-it
β β βββ markdown.js # Config markdown-it + plugin registration
β β βββ seo.js # Helper meta tag SEO
β βββ libs/markdown/ # Plugin kustom markdown-it (8 plugin)
β βββ GithubAnchor.js
β βββ TableOfContent.js
β βββ AdmonitionContainer.js
β βββ MacCodeBlock.js
β βββ MultiMediaPlayer.js
β βββ HighlightBacktick.js
β βββ Tabs.js
β βββ Dokapi.js
βββ views/ # Template EJS
β βββ layouts/
β β βββ base.ejs # Layout utama (html, head, body, navbar, footer)
β βββ pages/
β β βββ home.ejs # Halaman depan
β β βββ blog.ejs # Index blog
β β βββ post.ejs # Detail artikel blog
β β βββ docs.ejs # Index dokumentasi
β β βββ doc.ejs # Detail dokumentasi
β β βββ 404.ejs # Halaman tidak ditemukan
β β βββ 500.ejs # Error server
β βββ partials/
β βββ seo.ejs # Meta tag SEO (OG, Twitter, article)
β βββ navbar.ejs # Navigasi sticky
β βββ footer.ejs # Footer
β βββ post-card.ejs # Card artikel
β βββ project-card.ejs # Card proyek
βββ vercel.json # Config deploy Vercel
βββ package.json
βββ readme.md
βββ DESIGN-wise.md # Design system Wise-inspired
βββ ARSITEKTUR.md β file ini
Request masuk
β
ββ helmet() β security headers
ββ compression() β gzip
ββ express.static('/static') β file di public/
ββ render wrapper β nangkap res.render(), render page β bungkus di layouts/base.ejs
ββ initialization guard β tunggu data selesai di-load (cold start)
ββ injectLocals β navigation, socials, config, currentPath β res.locals
β
ββ Router (pages.js)
β ββ GET / β renderHome
β ββ GET /blog β renderBlogIndex
β ββ GET /blog/:slug β renderBlogPost
β ββ GET /docs β renderDocsIndex
β ββ GET /docs/:slug β renderDocDetail
β ββ GET /api/projects β apiProjects (JSON)
β
ββ notFound (404) jika tidak ada route cocok
ββ serverError (500) jika ada exception
- Cold start (pertama kali setelah deploy/idle):
initializeApplicationData()jalan β baca semua file JSON + glob markdown + parse semuanya sekali β simpan dicachein-memory. - Warm start (request berikutnya): guard
if (cache.blogs.length && cache.docs.length) returnβ skip, langsung pakai cache. - Guard middleware pakai
initPromiseagar request kedua menunggu request pertama selesai, bukan jalan duplikat.
Tidak ada file CSS statis. Semua styling via Tailwind CDN + inline class di EJS. Ini tempatnya:
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: { sans: ['Inter', ...] },
animation: { fadeIn: ... },
keyframes: { fadeIn: ... }
}
}
}
</script>Ganti font, animasi, atau kustomisasi Tailwind di sini.
Di bagian <style> inline:
/* Prose colors (light) */
.prose { --tw-prose-body: #454745; --tw-prose-headings: #0e0f0c; ... }
/* Prose colors (dark) */
.dark .prose { --tw-prose-body: #c8cac5; --tw-prose-headings: #e8ebe6; ... }
/* Syntax highlighting Prism (light & dark) */
.token.comment { color: #868685; ... }
.dark .token.comment { color: #6b6b6a; ... }<body class="bg-[#e8ebe6] dark:bg-[#0e0f0c] text-[#0e0f0c] dark:text-[#e8ebe6] ...">| File | Yang di-style |
|---|---|
views/pages/home.ejs |
Hero section, glow background, cards "Operational DNA", featured projects, latest posts |
views/pages/blog.ejs |
Search bar, sort dropdown, tag chips, grid post-card, modal "More tags" |
views/pages/post.ejs |
Cover image, meta info, tags, konten artikel (prose) |
views/pages/docs.ejs |
Grid card dokumentasi |
views/pages/doc.ejs |
Layout 2 kolom (artikel + sidebar), konten prose |
views/pages/404.ejs |
Angka besar 404, tombol Go Home |
views/pages/500.ejs |
Angka besar 500, error stack (dev mode) |
views/partials/navbar.ejs |
Sticky nav, logo, menu desktop/mobile, toggle dark mode |
views/partials/footer.ejs |
Copyright, sosial icons |
views/partials/post-card.ejs |
Card artikel (cover, title, desc, tags) |
views/partials/project-card.ejs |
Card proyek (cover, title, desc, tags) |
Plugin menghasilkan HTML dengan class Tailwind inline (server-side). Contoh:
| Plugin | File | Class Tailwind yang dipakai |
|---|---|---|
| GithubAnchor | GithubAnchor.js |
group-hover:opacity-100, no-underline |
| TableOfContent | TableOfContent.js |
bg-white/50, backdrop-blur, border-sky-200, text-sky-700 |
| AdmonitionContainer | AdmonitionContainer.js |
border-l-4, bg-sky-50, text-sky-800, dark:... |
| MacCodeBlock | MacCodeBlock.js |
rounded-2xl, bg-[#1e1e1e], shadow-lg |
| MultiMediaPlayer | MultiMediaPlayer.js |
class tailwind di wrapper video/audio |
| HighlightBacktick | HighlightBacktick.js |
bg-cyan-50, text-cyan-800, shadow-inner |
| Tabs | Tabs.js |
border-b-2, border-sky-500, text-sky-600 |
| Dokapi | Dokapi.js |
Badge warna per HTTP method, collapsible card |
| File | Fungsinya |
|---|---|
config.json |
siteName, siteTitle, siteDescription, siteUrl, avatar, fullName, occupation, bio |
navigation.json |
Array {label, href} untuk navbar |
socials.json |
Array {name, icon, url} untuk footer |
projects.json |
Array proyek dengan featured boolean |
Frontmatter (YAML):
---
title: "Judul"
description: "Deskripsi"
slug: "judul-artikel"
tags: ["tag1", "tag2"]
createdAt: "2025-07-01"
updatedAt: "2025-07-05"
written: "Nama Penulis"
pinned: false
draft: false
cover: "https://..."
---Kustom syntax markdown: [[toc]], :::note ... :::, ;;tabs ... ;;, code block dengan title="", & route ... &, < response ... <, dsb.
Semua data dari file β cache in-memory (cold start) β serve dari cache (warm start)
File:
data/config.json ββ
data/navigation.json ββ€ Promise.all() β cache.*
data/socials.json ββ€
data/projects.json ββ
content/blog/**/*.md ββ€ glob β Promise.all(parseMarkdown) β cache.blogs
content/docs/**/*.md ββ β cache.docs
cache = {
blogs: [{ title, slug, tags, createdAt, pinned, draft, cover, readingTime, html, ... }],
docs: [{ title, slug, tags, createdAt, readingTime, html, ... }],
projects: [{ id, title, description, image, tags, featured, href }],
socials: [{ name, icon, url }],
navigation: [{ label, href }],
config: { siteName, siteTitle, ... }
}
file.md
β gray-matter (parse frontmatter YAML)
β markdown-it + 8 plugin kustom β HTML
β hitung readingTime (200 kata/menit)
β objek { title, slug, tags, createdAt, ..., html }
vercel.json:
- Builder:
@vercel/nodeβsrc/app.js includeFiles: "content/**"β markdown ikut bundle- Rewrite:
/static/*βpublic/*, sisanya βsrc/app.js - Local dev:
node --watch src/app.js(port 3000)
Di Vercel (production), app di-export sebagai default β tidak pakai app.listen().
Di lokal (NODE_ENV !== 'production'), app.listen() jalan.
Dari DESIGN-wise.md β palet Wise:
| Token | Warna | Tailwind |
|---|---|---|
primary |
#9fe870 |
bg-[#9fe870] |
ink |
#0e0f0c |
text-[#0e0f0c] |
canvas-soft |
#e8ebe6 |
bg-[#e8ebe6] |
canvas |
#ffffff |
bg-white |
body |
#454745 |
text-[#454745] |
mute |
#868685 |
text-[#868685] |
Semua warna hardcoded sebagai bg-[#...] atau text-[#...] langsung di class Tailwind. Tidak pakai variable CSS β ganti warna dengan search-replace di seluruh file .ejs.
Ceil: Ekstrak ke CSS variables atau Tailwind
theme.extend.colorsdi config kalau warna dipakai > 20 kali. Kapan: Saat mau ganti tema secara sistematis.
| Plugin | Sintaks | Output |
|---|---|---|
| GithubAnchor | heading ## Teks |
auto id slug + link anchor # |
| TableOfContent | [[toc]] |
Dropdown collapsible daftar isi |
| AdmonitionContainer | :::note ... ::: |
Card berwarna dengan ikon |
| MacCodeBlock | js title="file.js" |
macOS-style terminal dengan copy & wrap |
| MultiMediaPlayer |  |
<video> / <audio> alih-alih <img> |
| HighlightBacktick | `code` |
Inline code dengan gaya cyan |
| Tabs | ;;tabs ... ;; |
Tab horizontal dengan animasi fade |
| Dokapi | & route GET /api ... & |
Card endpoint API collapsible |
| Method | Path | Controller | View |
|---|---|---|---|
| GET | / |
renderHome |
pages/home |
| GET | /blog |
renderBlogIndex |
pages/blog |
| GET | /blog/:slug |
renderBlogPost |
pages/post |
| GET | /docs |
renderDocsIndex |
pages/docs |
| GET | /docs/:slug |
renderDocDetail |
pages/doc |
| GET | /api/projects |
apiProjects |
JSON |
| * | static | express.static |
file di public/ |
| * | not found | notFound |
pages/404 |
| * | error | serverError |
pages/500 |