| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A personal blog and a minimal static site generator, all in one repo. Parses Markdown posts with frontmatter, renders them to HTML pages with syntax highlighting and table of contents, then deploys to GitHub Pages via CI.
The posts are mine; the generator code is yours if you want to use it. Create from template or fork it, remove my content, and have your own blog.
This could be a separate static site generator as a template, but it's small enough to be an npm dependency, hence, I keep them in one repo.
Another good thing about this is that we have two choices, either read the blogs in this repo as is, or view the rendered pages. Either way, enjoy !
Most static site generators (Jekyll, Hugo, Gatsby, Next.js) come with a full build system, plugins, themes, and opinions baked in. It was too much for me since I want minimalistic and be hands-on. So I built it from scratch.
If you want a blog that:
This does that with a small build script and plain CSS !
content/posts/*.md ─┐ build.js ├─► public/ (HTML, CSS, assets) static/ ─┘
blog/ ├── content/ │ └── posts/ ← Markdown files go here ├── static/ │ ├── style.css ← All blog styles │ └── hljs.css ← Syntax highlight theme ├── build.js ← The build script ├── .github/ │ └── workflows/ │ └── deploy.yml ← CI pipeline └── public/ ← Generated output (gitignored)
Each post file in content/posts/ must have valid YAML frontmatter. All fields are required unless noted:
---
title: "Post Title"
date: 2024-04-26
slug: post-slug
description: "Short description for SEO."
topics: [topic1, topic2]
---| Field | Required | Notes |
|---|---|---|
| title | Yes | Used in page title, H1, and meta tags |
| date | Yes | Format: YYYY-MM-DD. If missing, build time is used and a warning is shown |
| slug | No | URL slug. Defaults to the filename without .md. Must be unique across all posts |
| description | Yes | Meta description for SEO. If missing, the first paragraph is used (truncated to ~160 chars) and a warning is shown |
| topics | No | Array of topic labels, e.g. [ai, crypto]. Shown as chips on the index page. Topic pages are auto-generated |
Fallback behavior:
npm install # Install dependencies
npm run build # Build site to public/
npx serve public # Preview locally (serves on port 3000 by default)
npx serve public -l 3700 # serve on port 3700 or any other portPush to main, GitHub Actions handles the rest:
No manual deployments. No separate publishing step. Just push.
When deploying to a subdirectory (e.g., username.github.io/repo-name/), set BASE_PATH locally:
BASE_PATH=/repo-name npm run buildFor GitHub Pages root (username.github.io), leave it empty — the CI auto-detects from GITHUB_REPOSITORY.
For GitHub Pages to work, the repository must be public, and Pages must be enabled at Settings → Pages → Source: gh-pages branch. On the other hand, GitHub Pro lets you keep the source private while the rendered blog remains public.
If you want the source (posts, build script, drafts) private but the deployed site public:
Keep this repo private: your source, build script, drafts, work-in-progress posts
Create a separate public repository (e.g. my-blog): only receives the built output, serves at username.github.io/blog
In the workflow, use a Personal Access Token (PAT) with repo scope to push to the target repo:
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.PAT }}
publish_dir: ./public
destination_repository: username/my-blogAdd the PAT as a secret in the private repo (Settings → Secrets and variables → Actions)
The CI builds privately, then pushes the public/ output to the public my-blog repo. The source never leaves your private repository.
This blog + generator can handle years of regular writing:
| Library | Purpose |
|---|---|
| gray-matter | Frontmatter parsing |
| marked | Markdown to HTML |
| marked-highlight | Code highlight plugin |
| highlight.js | Syntax highlighting engine |
TOC anchor links and the browser Back button are inherently at odds, each TOC click pushes a history entry, so Back navigates through every clicked section before leaving the page. There are four ways to handle this:
1. History-free TOC (preventDefault) — chosen
document.addEventListener('click', function(e) {
var link = e.target.closest('[href^="#"]');
if (link) {
e.preventDefault();
var target = document.getElementById(link.getAttribute('href').slice(1));
if (target) { target.scrollIntoView(); }
}
});TOC clicks are intercepted, no history entry is created. Back always goes to the actual previous page. Browser's own history management preserves scroll position automatically.
Back button: referrer check -> if same-domain referrer exists, use history.back(), otherwise the <a> goes to index.
URL never shows #hash, loses shareable deep-links.
Middle-click, right-click, and modified clicks open the index in a new tab, Back button is for plain left-click only.
2. Native TOC (browser history) + referrer back
<a href="#heading-id">Section</a>TOC links work as normal anchors. Each click pushes a history entry with the #hash in the URL. Back button uses referrer to go back to previous page. URL is a shareable deep-link.
The tradeoff: when the user clicks 1 or more TOC sections, the Back button goes back through each TOC entry before reaching the previous page, which is not ideal.
3a. Read-only TOC (static, no JS)
<li>Section Name</li> <!-- plain text, no href -->Decorative outline. No clicks, no JS. Just shows page structure.
3b. Read-only TOC + scroll tracking (JS)
Same as 3a, but with scrollspy JS that highlights the current section as the user scrolls. Adds a scroll listener for a "you are here" cue.
Smooth scroll for all anchor links (optional):
html { scroll-behavior: smooth; }Makes both native anchor links and scrollIntoView() feel consistent across all browsers.
| Back | FazBrowse Home | New Git URL |