Markdown blog

Statik sitenin en tipik kullanımı: bir klasördeki markdown dosyaları, yazı başına bir statik sayfa, bir RSS akışı ve sıfır istemci tarafı JavaScript. Tam kaynak examples/blog içinde.

Elinize geçenler

Proje düzeni

my-blog/
  sitelo.config.js
  content/
    hello-world.md       # posts: frontmatter + markdown
    why-static.md
  src/
    lib/
      posts.js           # content/ dizinini okur, frontmatter’ı ayrıştırır, markdown’ı işler
    index.ht.js          # / — yazı listesi
    blog/
      [slug].ht.js       # /blog/:slug — yazı başına bir sayfa
    css/
      styles.css
export default {
  site: 'https://example.com',
  rss: {
    site: 'https://example.com',
    title: 'Blogum',
    description: 'Son yazılar',
    routePrefix: '/blog', // her /blog/* sayfası bir akış ögesi olur
  },
}

1. Yazıları markdown olarak yazın

Yazılar content/ içinde yaşar — src/ dışında, böylece sitelo onları hiçbir zaman sayfa ya da varlık saymaz. Frontmatter düz key: value satırlarıdır:

---
title: Merhaba, dünya
date: 2026-08-01
description: Kaçınılmaz ilk yazı.
---

Bu blog, statik HTML’e işlenen bir markdown dosyaları klasörüdür.

2. Onları Node’da okuyun ve işleyin

Yalnızca sunucuya ait küçük bir modül klasörü okur, frontmatter’ı ayrıştırır ve markdown’ı marked ile işler. HTML içinde hiçbir şey bu modüle başvurmadığından tarayıcıya hiç gönderilmez.

import { readdir, readFile } from 'node:fs/promises'
import { marked } from 'marked'

// Yazılar src/ dışında durur, böylece hiçbir zaman sayfa ya da varlık sayılmazlar.
const CONTENT_DIR = new URL('../../content/', import.meta.url)

/** Minik frontmatter ayrıştırıcı — --- çitleri arasındaki `anahtar: değer` satırları. */
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) }
}

/** Tüm yazılar, en yeniden başlayarak. Yalnızca derleme/geliştirme sırasında Node’da çalışır. */
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. Yazıları ana sayfada listeleyin

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: 'tr' },
    head(
      title('Blogum'),
      link({ rel: 'stylesheet', href: '/styles.css' }),
      link({
        rel: 'alternate',
        type: 'application/rss+xml',
        title: 'Blogum',
        href: '/rss.xml',
      }),
    ),
    body(
      h1('Blogum'),
      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' }, 'RSS akışı')),
    ),
  )

4. Yazı başına bir statik sayfa

generateStaticParams derleme sırasında her slug’ı döndürür; data() her sayfa için eşleşen yazıyı yükler.

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(`Yazı bulunamadı: ${params.slug}`)
  return { post }
}

export default ({ data }) => {
  const { post } = data

  return html({ lang: 'tr' },
    head(
      title(`${post.title} — Blogum`),
      meta({ name: 'description', content: post.description }),
      link({ rel: 'stylesheet', href: '/styles.css' }),
    ),
    body(
      article(
        p(a({ href: '/' }, '← Tüm yazılar')),
        h1(post.title),
        time({ datetime: post.date }, post.date),
        post.html,
      ),
    ),
  )
}

5. Bedavaya RSS

Yukarıdaki rss yapılandırmasıyla sitelo build, /blog altındaki her sayfa için bir öğe içeren dist/rss.xml üretir — fazladan kod yok.

Yönlendirme belgeleri · Veri yükleme belgeleri · Yapılandırma belgeleri