Skip to content

Repository files navigation

What is this?

A minimal, fast static site generator for my personal blog. It was inspired by Hugo, built with TypeScript (run via tsx) and Tailwind CSS v4: no framework, just template functions writing HTML files.

Features

  • Markdown posts with YAML frontmatter
  • Atom feed: feed.xml with auto-discovery (<link rel="alternate">), drafts excluded
  • SEO basics: sitemap.xml (posts, tags, categories) and robots.txt generated at build time
  • Rich social + search metadata: complete OpenGraph/Twitter tags (og:type="article", article:*, Twitter cards) and JSON-LD BlogPosting structured data on every post
  • Generated social cards: each post gets a 1200x630 title card rendered at build time (SVG via sharp), used as its share preview
  • Syntax highlighting via highlight.js (GitHub Dark Dimmed theme)
  • Image pipeline: raster images get optimized WebP variants at build time (sharp); post HTML is rewritten automatically, originals pruned from the output
  • Tailwind CSS v4 with @tailwindcss/typography for pretty prose
  • Auto-extracted metadata: title, date, tags, author, description, reading time
  • Categories: automatically generated from posts' metadata
  • Tags: automatically generated from posts' metadata
  • Related posts: up to three, scored by shared category and tags
  • GitHub Pages ready: includes a CI/CD workflow

Project Structure

my-blog/
|-- content/
|   `-- posts/          <- blog markdowns
|-- src/
|   |-- scripts/
|   |   |-- types.ts        <- shared TS types
|   |   |-- parser.ts       <- markdown + frontmatter parser
|   |   |-- renderer.ts     <- HTML template functions
|   |   `-- build.ts        <- build orchestrator (dev + prod)
|   |-- utils/
|   |   `-- lib.ts          <- site config + shared helpers
|   |-- tests/
|   |   |-- parser.test.ts
|   |   `-- lib.test.ts
|   |-- styles/
|   |   `-- main.css    <- tailwind entry + all custom CSS (mostly not written by me)
|-- public/             <- static assets copied as-is to dist/
|-- dist/               <- generated output (not meant to be edited manually)
|-- .github/
|   `-- workflows/
|   `-- deploy.yml  <- GitHub Actions CI/CD
|-- .oxlintrc.json      <- oxlint configuration
|-- .prettierrc.json    <- Prettier configuration
|-- package.json        <- dependencies (managed with pnpm)
`-- tsconfig.json       <- TS configuration

Getting Started

1. Install dependencies

Requires pnpm (corepack enable works too):

pnpm install

2. Run locally

pnpm dev

This starts three concurrent processes:

  • build watcher: rebuilds HTML when markdown or templates change
  • CSS watcher: rebuilds Tailwind CSS on source change
  • local server: serves dist/ at http://localhost:3000

3. Production build

pnpm build

Generates optimized output in dist/. Preview it locally:

pnpm preview

Writing Posts

Create a new .md file in content/posts/:

---
title: "My New Post"
date: "2026-01-31"
description: "A short summary shown in listings."
category: "category-name"
tags: ["tag-one", "tag-two"]
author: "Your Name"
draft: false
---

Your post content here...

The filename becomes the URL slug. my-new-post.md -> /posts/my-new-post/.

Frontmatter fields

Field Required Description
title yes Post title
date yes ISO date string (YYYY-MM-DD)
description no Excerpt for listings and meta tags
tags no Array of strings
category no Defaults to "uncategorized"
author no Defaults to "Anonymous"
lang no BCP 47 tag (e.g. ja); defaults to "en"
draft no true hides post in prod builds

Code blocks

Fenced code blocks with a language identifier get syntax-highlighted:

```typescript
const greeting = (name: string) => `Hello, ${name}!`;
```

Customising the Site

Site title, author, description

Edit getSiteConfig() in src/utils/lib.ts:

export function getSiteConfig(): SiteConfig {
  return {
    title: "My Blog",
    description: "A personal blog about code and ideas.",
    author: "Your Name",
    baseUrl: process.env["BASE_URL"] ?? "",
    siteUrl: process.env["SITE_URL"] ?? "https://your-site.example.com",
    year: new Date().getFullYear(),
    socials: {
      github: "https://github.com/you/",
      linkedin: "https://www.linkedin.com/in/you/",
      email: "you@example.com",
    },
  };
}

baseUrl prefixes internal links (leave empty for root-relative URLs); siteUrl is the absolute origin used for metadata like <link rel="canonical">.

Images

Drop images in public/<post-slug>/ and reference them as usual (![alt](/post-slug/1.png)). At build time every .png/.jpg under public/ gets a WebP sibling generated with sharp (max width 1920px, quality 80), image tags in posts are rewritten to use it, and the original is removed from dist/; an original is only kept if its WebP encoding failed. Variants are cached by mtime, so unchanged images aren't re-encoded on rebuilds. Keep in mind direct links to old .png/.jpg URLs will no longer resolve.

Comments (giscus)

Comments are powered by giscus (GitHub Discussions) and appear at the bottom of every post. To enable them:

  1. Make sure your repo is public and has Discussions enabled (Settings > General > Features)
  2. Install the giscus app
  3. Pick a Discussion category: Announcements is recommended so only you can open threads
  4. Fill in the IDs from giscus.app in getSiteConfig() (src/utils/lib.ts): repoId and categoryId

Until both IDs are set, the comments section stays hidden.

Styles

All styling lives in src/styles/main.css. The design tokens (colors, fonts, spacing) are defined as CSS custom properties in the @theme block at the top of the file. Change them to theme the whole site.

Adding pages

To add a new static page (e.g. /uses/):

  1. Add a renderUses(config: SiteConfig): string function in src/scripts/renderer.ts
  2. Call it in src/scripts/build.ts inside the build() function:
writeFile(path.join(OUT_DIR, "uses", "index.html"), renderUses(config));
  1. Add a nav link in the navbar() function in renderer.ts

Scripts Reference

Command Description
pnpm dev Dev server + watchers at localhost:3000
pnpm build Production build to dist/
pnpm preview Serve dist/ locally
pnpm type-check TypeScript type checking
pnpm lint Lint with oxlint (pnpm lint:fix auto-fixes)
pnpm format Format with Prettier (pnpm format:check verifies)
pnpm test Run tests with vitest (pnpm test:watch for watch)

Tooling

  • pnpm: package manager, pinned via the packageManager field in package.json (Corepack picks it up automatically)
  • oxlint: linter, configured in .oxlintrc.json
  • Prettier: formatter, configured in .prettierrc.json; ignores dist/, public/, and post markdown (see .prettierignore)

About

My blog site with my custom static site generator.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages