Blog Markdown
Di halaman ini
Kasus pemakaian situs statis yang paling khas: berkas markdown dalam sebuah folder, satu halaman statis per pos, umpan RSS, dan nol JavaScript sisi klien. Sumber lengkap di examples/blog.
Yang Anda dapat
- Halaman beranda yang mendaftar pos, terbaru lebih dulu
/blog/[slug]— satu halaman HTML statis per berkas markdown lewatgenerateStaticParamsrss.xml— dihasilkan sitelo dari konfigurasirsssitemap.xml— diaktifkan dengan menyetelsite- Nol JS diterbitkan — penguraian markdown terjadi saat build di Node
Tata letak proyek
my-blog/
sitelo.config.js
content/
hello-world.md # posts: frontmatter + markdown
why-static.md
src/
lib/
posts.js # baca content/, urai frontmatter, render markdown
index.ht.js # / — daftar tulisan
blog/
[slug].ht.js # /blog/:slug — satu halaman per tulisan
css/
styles.cssexport default {
site: 'https://example.com',
rss: {
site: 'https://example.com',
title: 'Blog saya',
description: 'Tulisan terbaru',
routePrefix: '/blog', // setiap halaman /blog/* menjadi item feed
},
}1. Tulis pos sebagai markdown
Pos tinggal di content/ — di luar src/, jadi sitelo tidak pernah menganggapnya halaman atau aset. Frontmatter berupa baris key: value biasa:
---
title: Halo, dunia
date: 2026-08-01
description: Tulisan pertama yang wajib ada.
---
Blog ini adalah folder berisi berkas markdown yang dirender ke HTML statis.2. Baca dan render di Node
Modul kecil khusus server membaca foldernya, mengurai frontmatter, dan merender markdown dengan marked. Karena tidak ada di HTML yang merujuk modul ini, ia tidak pernah sampai ke peramban.
import { readdir, readFile } from 'node:fs/promises'
import { marked } from 'marked'
// Tulisan berada di luar src/ sehingga tidak pernah dianggap halaman atau aset.
const CONTENT_DIR = new URL('../../content/', import.meta.url)
/** Pengurai frontmatter mungil — baris `kunci: nilai` di antara pagar ---. */
function parseFrontmatter(raw) {
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw)
if (!match) return { meta: {}, body: raw }
const meta = {}
for (const line of match[1].split('\n')) {
const colon = line.indexOf(':')
if (colon === -1) continue
meta[line.slice(0, colon).trim()] = line.slice(colon + 1).trim()
}
return { meta, body: raw.slice(match[0].length) }
}
/** Semua tulisan, terbaru dulu. Berjalan di Node hanya saat build/dev. */
export async function getPosts() {
const files = (await readdir(CONTENT_DIR)).filter((file) =>
file.endsWith('.md'),
)
const posts = await Promise.all(
files.map(async (file) => {
const raw = await readFile(new URL(file, CONTENT_DIR), 'utf8')
const { meta, body } = parseFrontmatter(raw)
return {
slug: file.replace(/\.md$/, ''),
title: meta.title ?? file,
date: meta.date ?? '1970-01-01',
description: meta.description ?? '',
html: marked.parse(body),
}
}),
)
return posts.sort((a, b) => b.date.localeCompare(a.date))
}
export async function getPost(slug) {
const posts = await getPosts()
return posts.find((post) => post.slug === slug) ?? null
}3. Daftarkan pos di beranda
import { getPosts } from './lib/posts.js'
export async function data() {
return { posts: await getPosts() }
}
export default ({ data }) => `
<html lang="id">
<head>
<title>Blog saya</title>
<link rel="stylesheet" href="/css/styles.css">
<link rel="alternate" type="application/rss+xml" title="Blog saya" href="/rss.xml">
</head>
<body>
<h1>Blog saya</h1>
<ul class="posts">
${data.posts
.map(
(post) => `
<li>
<a href="/blog/${post.slug}">${post.title}</a>
<time datetime="${post.date}">${post.date}</time>
<p>${post.description}</p>
</li>`,
)
.join('')}
</ul>
<p><a href="/rss.xml">Feed RSS</a></p>
</body>
</html>
`import {
html, head, title, link, body, h1, ul, li, a, time, p,
} from 'javascript-to-html'
import { getPosts } from './lib/posts.js'
export async function data() {
return { posts: await getPosts() }
}
export default ({ data }) =>
html({ lang: 'id' },
head(
title('Blog saya'),
link({ rel: 'stylesheet', href: '/styles.css' }),
link({
rel: 'alternate',
type: 'application/rss+xml',
title: 'Blog saya',
href: '/rss.xml',
}),
),
body(
h1('Blog saya'),
ul({ class: 'posts' },
...data.posts.map((post) =>
li(
a({ href: `/blog/${post.slug}` }, post.title),
time({ datetime: post.date }, post.date),
p(post.description),
),
),
),
p(a({ href: '/rss.xml' }, 'Feed RSS')),
),
)import { getPosts } from './lib/posts.js'
export async function data() {
return { posts: await getPosts() }
}
export default function Home({ data }) {
return (
<html lang="id">
<head>
<title>Blog saya</title>
<link rel="stylesheet" href="/css/styles.css" />
<link
rel="alternate"
type="application/rss+xml"
title="Blog saya"
href="/rss.xml"
/>
</head>
<body>
<h1>Blog saya</h1>
<ul className="posts">
{data.posts.map((post) => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
<time dateTime={post.date}>{post.date}</time>
<p>{post.description}</p>
</li>
))}
</ul>
<p><a href="/rss.xml">Feed RSS</a></p>
</body>
</html>
)
}4. Satu halaman statis per pos
generateStaticParams mengembalikan setiap slug saat build; data() memuat pos yang cocok untuk tiap halaman.
import { getPost, getPosts } from '../lib/posts.js'
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPost(params.slug)
if (!post) throw new Error(`Tulisan tidak ditemukan: ${params.slug}`)
return { post }
}
export default ({ data }) => {
const { post } = data
return `
<html lang="id">
<head>
<title>${post.title} — Blog saya</title>
<meta name="description" content="${post.description}">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<article>
<p><a href="/">← Semua tulisan</a></p>
<h1>${post.title}</h1>
<time datetime="${post.date}">${post.date}</time>
${post.html}
</article>
</body>
</html>
`
}import {
html, head, title, meta, link, body, article, p, a, h1, time,
} from 'javascript-to-html'
import { getPost, getPosts } from '../lib/posts.js'
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPost(params.slug)
if (!post) throw new Error(`Tulisan tidak ditemukan: ${params.slug}`)
return { post }
}
export default ({ data }) => {
const { post } = data
return html({ lang: 'id' },
head(
title(`${post.title} — Blog saya`),
meta({ name: 'description', content: post.description }),
link({ rel: 'stylesheet', href: '/styles.css' }),
),
body(
article(
p(a({ href: '/' }, '← Semua tulisan')),
h1(post.title),
time({ datetime: post.date }, post.date),
post.html,
),
),
)
}import { getPost, getPosts } from '../lib/posts.js'
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export async function data({ params }) {
const post = await getPost(params.slug)
if (!post) throw new Error(`Tulisan tidak ditemukan: ${params.slug}`)
return { post }
}
export default function Post({ data }) {
const { post } = data
return (
<html lang="id">
<head>
<title>{post.title} — Blog saya</title>
<meta name="description" content={post.description} />
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<article>
<p><a href="/">← Semua tulisan</a></p>
<h1>{post.title}</h1>
<time datetime={post.date}>{post.date}</time>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
</body>
</html>
)
}5. RSS secara cuma-cuma
Dengan konfigurasi rss di atas, sitelo build menghasilkan dist/rss.xml dengan satu item untuk setiap halaman di bawah /blog — tanpa kode tambahan.
Dokumentasi perutean · Dokumentasi pemuatan data · Dokumentasi konfigurasi