In-depth AI/ML paper reviews, summaries, and tech guides β published as a blog.
A Jekyll static site, deployed to GitHub Pages.
π°π· νκ΅μ΄ README
If you've never run a Jekyll site before, here's the whole loop.
1. Install Ruby 3.3+ and Bundler. Check what you have:
ruby --version # need 3.3 or newer (see .ruby-version)
bundle --version # ships with Ruby; if missing: gem install bundlerOn macOS the system Ruby is old β use rbenv or
asdf to install 3.3+. (The repo pins the version in .ruby-version, so a version
manager will pick it up automatically.)
2. Install the project's gems (Jekyll, plugins, html-proofer):
bundle install3. Run the dev server. It rebuilds on save and serves at
http://localhost:4000:
bundle exec jekyll serveEdit a file under _posts/, _sass/, or _includes/, save, and refresh β most
changes appear immediately. (Changes to _config.yml need a server restart.)
4. Build for production (what CI does) when you want the final output in
_site/:
bundle exec jekyll buildWhy plain
jekylland notgithub-pages? This site uses custom Ruby plugins in_plugins/, which the sandboxedgithub-pagesgem disallows. So both local builds and CI run Jekyll directly.
_posts/ Posts β YYYY-MM-DD-slug.md (Korean; English twin is -en.md)
_layouts/ Page templates: default β post / page
_includes/ Reusable fragments: head, header, footer, nav_links,
page_divider, category-posts, language_switcher,
related_posts
_sass/ Styles: _layout, _post, _tags, _syntax (Rouge code theme),
_dark (dark mode), base/*
β bourbon/ and neat/ are vendored frameworks β don't edit
_plugins/ reading_time.rb (KO/EN-aware read time)
lazy_images.rb (adds loading="lazy" to <img>)
post_description.rb (fills page.description for posts)
related_posts.rb (page.related, and the prev/next links)
scrollable_tables.rb (wraps wide tables so they scroll)
search_index.rb (plain_text filter for search.json)
css/ main.scss (Sass entry point) Β· search.css (search page only)
js/ main.js (theme toggle, code-copy, TOC, menu, image zoomβ¦)
search.js (drives the search box)
assets/images/ Shared cover images, reused across posts by topic
assets/<slug>/ Per-post figures, one folder per post
search.json Full-text search index (consumed by simple-jekyll-search)
test/ minitest unit tests for the _plugins/ logic
script/ validate-site.sh (post-build discoverability checks)
sitemap-index.xml Sitemap index β the URL to submit to Search Console
.github/workflows/ CI: tests β build β html-proofer β validate-site; deploys on push to main
Top-level pages: index.html (home), plus paper-reviews.md,
paper-summaries.md, tech-guides.md, insights.md (the four section pages),
categories.html, tags.html, search.md, and about.md.
The easiest path is the /write-post skill, which runs the whole
research β draft β proofread workflow. To add one by hand, create
_posts/YYYY-MM-DD-slug.md starting with this front matter:
---
layout: post
title: "<Post Title>"
subtitle: "<one-line pitch>" # optional β shown under the title in the header
date: YYYY-MM-DD HH:MM:SS
author: "<Author>" # the paper's org; omit for Insights/opinion posts
description: >- # optional β see below
<search-snippet, ~150 chars>
categories: ["<Type>", "<Topic>"]
tags: ["<Tag-1>", "<Tag-2>"]
cover: /assets/images/<topic>.(jpg|png)
use_math: true # ONLY if the post has equations (loads MathJax)
lang: ko # optional β with translation_id belowβ¦
translation_id: <shared-slug> # β¦links a Korean post to its -en twin
---Don't repeat the title as an H1 in the body. The layout already renders it,
so a leading # Title produces two <h1>s and leaks into the search snippet.
Put a tagline in subtitle: instead.
description: is what Google shows under the link, what social cards quote, and
what the RSS <summary> carries. If you omit it, _plugins/post_description.rb
derives one from the post's first real prose paragraph, which is usually good
enough. Write it by hand when the first paragraph opens on a pull quote or a
disclosure note β that is, on most Insights posts.
Descriptions must be unique across the site; CI fails the build if two pages share one.
Categories are two levels:
categories[0]β the type:Paper Reviews,Paper Summaries,Tech Guides, orInsights. This decides which nav tab the post appears under. A type with no posts yet keeps its tab and renders an empty-state line: a missing tab reads as a section that was removed, not one still filling up.categories[1]β the topic:Language-Models,Multimodal-Learning,Finetuning,Retrieval-Augmented-Generation,Agentic-AI, β¦ (add new ones freely).
Jekyll combines the two with the date to build the output path:
categories: ["Paper Reviews", "Language-Models"] + date: 2025-01-23
β
_site/paper reviews/language-models/2025/01/23/<slug>.html
So changing the categories or date of a published post changes its URL, which breaks inbound links and search results. Set them once and leave them.
Tags are free-form and hyphenated, and a tag phrased as one paper's contribution
(Fine-Grained-Expert-Segmentation) can only ever apply to that paper. Those
make a precise index and connect nothing, and _plugins/related_posts.rb
requires a shared tag β so a post tagged only that way ships with no
"Related reading" block at all.
So also give each post at least one tag from the controlled topic layer:
Agentic-AI Alignment DeepSeek Knowledge-Graph Llama
Mixture-of-Experts Multimodal-Models Reasoning-Models
Retrieval-Augmented-Generation
Keep the specific tags β they say something the topic tag does not, and dropping
them would move live /tags/ anchors. Add the topic tag, don't swap for it.
Before coining a new topic tag, check that nothing above already covers it. Three
names for one idea (Agentic-Architecture, Agentic-Patterns,
Agentic-Infrastructure) leave every post holding a tag no other post shares,
which is the same as having no topic tag at all.
Write $$β¦$$ for both inline and display math, and set use_math: true.
Never use single $β¦$. kramdown doesn't treat single $ as math, so its
Markdown pass turns _/* inside the span into <em>/<strong> before
MathJax runs β e.g. $a*b*c$ becomes $a<em>b</em>c$ and renders broken. With
$$, kramdown emits verbatim \(β¦\) and leaves the contents alone. (Prose
dollar signs like $10M are fine β they're not math.)
These are the four gates CI runs, in the same order. Build to a throwaway
directory rather than _site/, for the reason in the note below:
ruby test/run_all.rb # plugin logic still correct?
bundle exec jekyll build --strict-front-matter \
--destination /tmp/site-verify # does it build clean?
bundle exec htmlproofer /tmp/site-verify --disable-external \
--allow-hash-href --no-enforce-https # broken links, images, anchors?
script/validate-site.sh /tmp/site-verify # sitemap, feed, metadata, headingsIf a check reports something impossible, look for a running
jekyll servefirst. It watches the tree and rewrites_site/behind you, it overridessite.urlwithhttp://localhost:4000(so every sitemap URL looks wrong), and it holds the_config.ymlit started with β soexcludeentries added since then don't apply. Building elsewhere sidesteps all three:ps aux | grep '[j]ekyll serve'
test/ unit-tests the pure logic in _plugins/ β one file per plugin. Plain
ruby, not bundle exec: each plugin guards its Jekyll/Liquid registration
behind defined? so the logic loads standalone, and minitest ships with Ruby.
Anything you change in _plugins/ changes every page on the site, so add a
case before changing behaviour.
.github/workflows/jekyll.yml runs on pull requests to main as well as
pushes to it, so the gates below block a bad merge rather than merely reporting
one after the fact. Steps 1β4 run on both events; step 5 is skipped for pull
requests. In order, it:
- runs
ruby test/run_all.rb(the_plugins/unit tests), - builds the site with
JEKYLL_ENV=production, - runs html-proofer over
_site/(internal links, images, anchors), - runs
script/validate-site.shβ sitemap/feed parse at byte 0, every sitemap URL under the configuredurl, a pinned build timezone,robots.txtnot blocking, at least one rendered page, exactly oneh1per page, no heading-level skips, a description and a canonical on every page, every description longer than its own title, no duplicate description or title, and no authoring sources published β and - deploys to GitHub Pages.
A failure is almost always step 3 or 4; the Actions log names the exact link, image, or page. There is no manual deploy step.
β Don't add
google*.html/naver*.htmlto_config.yml'sexclude. They're Search Console / Naver ownership-verification tokens that must ship to the site root. Excluding them silently breaks ownership verification.
Check the file first β it is usually fine:
curl -sI https://bits-bytes-nn.github.io/sitemap.xml # expect 200, application/xml
curl -sS https://bits-bytes-nn.github.io/sitemap.xml -o /tmp/s.xml && \
ruby -rrexml/document -e 'REXML::Document.new(File.read("/tmp/s.xml")); puts "well-formed"'
curl -sS https://bits-bytes-nn.github.io/robots.txtIf those pass, the failure is a cached Search Console verdict, not the site.
Search Console keys a sitemap by URL and keeps the first result it recorded, so
re-submitting the same path reuses the stale entry. Remove the entry and submit
sitemap-index.xml instead β a URL it has not seen before β then use URL
Inspection β Request Indexing on a couple of posts to prompt a crawl.
MIT β see LICENSE. Built on the Centrarium Jekyll theme.
