Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/jekyll.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ jobs:
id: pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- name: Build with Jekyll
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" --strict-front-matter
run: bundle exec jekyll build --baseurl "$BASE_PATH" --strict-front-matter
env:
JEKYLL_ENV: production
BASE_PATH: ${{ steps.pages.outputs.base_path }}
- name: Validate HTML, internal links, and images
# Catches broken internal links/anchors and missing images before deploy.
# External links are skipped (slow, flaky) but must be https; hash-only
Expand All @@ -80,7 +81,9 @@ jobs:
bundle exec htmlproofer ./_site \
--disable-external \
--allow-hash-href \
--swap-urls "^${{ steps.pages.outputs.base_path }}:"
--swap-urls "^$BASE_PATH:"
env:
BASE_PATH: ${{ steps.pages.outputs.base_path }}
- name: Validate discoverability (sitemap, feed, metadata)
# Guards what htmlproofer does not look at: that the sitemap and feed
# parse, that every page carries a description and a canonical, that no
Expand Down
61 changes: 26 additions & 35 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ js/ main.js(테마 토글·코드 복사·목차·메뉴·이미
assets/images/ 토픽별 커버 이미지와 글의 그림·다이어그램(플랫)
assets/<slug>/ 일부 오래된 글의 그림 폴더. 새 글은 assets/images/를 씀
search.json 전체 본문 검색 색인 (simple-jekyll-search가 사용)
test/ _plugins/ 로직의 minitest 단위 테스트
test/ _plugins/와 글 린트의 minitest 단위 테스트
script/ lint-posts.rb (빌드 전 글 원본 규칙 검사)
validate-site.sh (빌드 후 검색 노출 검사)
check-plugin-output.rb (플러그인 훅이 글에 남긴 결과 검사,
Expand All @@ -108,22 +108,22 @@ _data/ post_conventions.yml (카테고리·표지·통제 태그·

## 글 작성하기

가장 쉬운 방법은 `/write-post` 스킬입니다. 리서치 → 초안 → 퇴고까지 한 번에 진행해 줍니다.
직접 만들 때는 `_posts/YYYY-MM-DD-slug.md` 파일을 만들고 아래 프런트매터로 시작하세요.
논문 리뷰는 scholar-lens가 PR로 올리고, 나머지 글은 `/write-post` 스킬(로컬 전용,
gitignore된 `.claude/` 아래)이 리서치 → 초안 → 퇴고를 진행합니다. 직접 만들 때는 `_posts/YYYY-MM-DD-slug.md` 파일을 만들고 아래 프런트매터로 시작하세요.

```yaml
---
layout: post
title: "<글 제목>"
subtitle: "<한 줄 소개>" # 선택 — 헤더의 제목 아래에 표시됩니다
date: YYYY-MM-DD HH:MM:SS
paper_author: "<기관>" # 논문을 낸 기관(사람 이름 아님). Insights·의견 글이면 생략
paper_author: "<기관>" # 논문을 낸 기관(사람 이름 아님). 논문 글에만
seo_title: "<짧은 제목>" # 선택 — 논문 제목이 너무 길 때 <title>에 대신 씁니다
description: >- # 선택, 평문 — 아래 설명 참고
<검색 결과에 노출될 문장, 150자 안팎>
categories: ["<유형>", "<주제>"]
tags: ["<태그-1>", "<태그-2>"]
cover: /assets/images/<topic>.(jpg|png) # Insights는 insights.jpg
cover: /assets/images/<topic>.(jpg|png) # 유형이나 주제의 이미지, 아래 참고
use_math: true # 수식이 있을 때만 (MathJax를 불러옵니다)
lang: ko # translation_id를 쓰면 필수(ko | en)…
translation_id: <공통-슬러그> # …한국어 글과 -en 번역본을 연결합니다.
Expand All @@ -146,17 +146,10 @@ RSS `<summary>`에 실리는 문장입니다. 생략하면 `_plugins/post_descri
첫 산문 단락에서 뽑아 주는데, 대개 그걸로 충분합니다. 다만 첫 단락이 인용문이나 공개
고지로 시작하는 글 — 즉 대부분의 Insights 글 — 은 직접 쓰는 편이 낫습니다.

설명문은 **사이트 전체에서 중복되면 안 됩니다.** 두 페이지가 같은 설명문을 쓰면 CI가
설명문은 **사이트 전체에서 중복되면 안 되고** 페이지 제목보다 길어야 합니다. 어기면 CI가
빌드를 실패시킵니다. 설명문은 평문으로 나가므로 `$$`, `*강조*`, 백틱, 링크, 태그를 쓰면
린트가 막습니다.

### 날짜, 파일명, Liquid

파일명은 게시일(`YYYY-MM-DD-`)로 시작합니다. `date:`는 그보다 이를 수(논문 발행일)는 있어도
늦을 수는 없습니다. Jekyll은 모든 글을 먼저 Liquid로 렌더하므로, 코드 블록의 `{{ … }}`나
`{% … %}`(프롬프트 템플릿, GitHub Actions의 `${{ … }}`)는 빈칸이 되거나 빌드를 멈춥니다.
`{% raw %}…{% endraw %}`로 감쌉니다. 세 가지 모두 린트가 확인합니다.

### 카테고리가 URL을 결정합니다

카테고리는 **두 단계**입니다.
Expand All @@ -166,8 +159,9 @@ RSS `<summary>`에 실리는 문장입니다. 생략하면 `_plugins/post_descri
**탭을 유지하고** 빈 상태 문구를 띄웁니다 — 탭이 없으면 채우는 중인 섹션이 아니라
없어진 섹션으로 읽히기 때문입니다.
- `categories[1]` — **주제**: `_data/post_conventions.yml`의 `topics` 중 하나이며, 표지
이미지도 거기서 정해집니다(Insights 글은 유형의 `insights.jpg`를 씁니다). 맞는 주제가 없으면 먼저 거기에 추가합니다. 목록에 없는 값은
린트가 막습니다.
이미지도 거기서 정해집니다. Insights 글은 유형의 `insights.jpg`를 쓰고, 이미지가 없는
주제는 저장소에 있는 아무 이미지나 씁니다. 맞는 주제가 없으면 먼저 거기에 추가합니다.
목록에 없는 값은 린트가 막습니다.

Jekyll은 이 둘을 슬러그로 바꾸고(`_config.yml`의 `permalink`) 날짜와 합쳐 출력 경로를
만듭니다.
Expand Down Expand Up @@ -216,26 +210,23 @@ _site/paper-reviews/language-models/2025/01/23/<slug>.html
kramdown이 내용을 그대로 보존해 `\(…\)`로 내보내므로 안전합니다. (`$10M` 같은 본문 속
달러 표기는 수식이 아니니 그대로 둬도 됩니다.)

린트는 렌더링이 깨지는 다음 경우도 막습니다.

- `\\(…\\)`·`\\[…\\]`로 쓴 수식, 단일 `$` 옆의 TeX: 위와 같이 마크다운이 수식을 먼저
읽습니다.
- `$$…$$` 안의 `\_`: MathJax가 아래첨자가 아닌 밑줄 문자를 그대로 그립니다. `_`로 쓰고,
`\text{…}` 안에서만 이스케이프합니다.
- 앞뒤에 빈 줄이 없는 디스플레이 `$$…$$` 줄(목록 안 포함): kramdown이 문단에 붙여 인라인
수식으로 처리합니다.
- `$$` 구간 안의 한글: 거의 언제나 통화 표기 `$$` 두 개가 한 구간으로 읽힌 것입니다.
- `use_math: true` 없이 쓴 `$$` 수식: MathJax가 로드되지 않아 TeX가 글자로 보입니다.
- HTML이 아닌 `<이름>`, `<s>`·`<input>`, 또는 속성 없는 `<img>`(모델의 특수 토큰): kramdown이 그대로
넘기고 브라우저가 지웁니다. `코드`로 씁니다.
- 마커 뒤에 언어 말고 다른 것이 붙은 코드 펜스(```` ```python title="a" ````)나 닫히지 않은
펜스: kramdown이 펜스로 읽지 않습니다.
- 글마다 대소문자를 다르게 쓴 태그(`Grouped-Query-…`와 `Grouped-query-…`).
- 표 행의 `$$` 안에 있는 `|`: kramdown이 거기서 셀을 나누고, `\|`는 `|`로 바꿉니다(노름이
절댓값이 됩니다). `\lVert … \rVert`를 씁니다.

인라인 수식에서는 맨 `|` 대신 `\lvert … \rvert`(또는 `\mid`)를, 노름에는 `\lVert … \rVert`를 씁니다. 린트는 표 안에서만
잡습니다.
수식 안의 막대는 `\lvert … \rvert`나 `\mid`로, 노름은 `\lVert … \rVert`로 쓰고 맨 `|`는
쓰지 않습니다. 표 행에서는 kramdown이 거기서 셀을 나눕니다.

### 린트가 잡는 그 밖의 것

`ruby script/lint-posts.rb`는 찾은 줄과 고치는 방법을 함께 알려 줍니다. 자주 걸리는
규칙은 다음과 같습니다. 모두 다른 검사는 통과하는데 페이지가 잘못 렌더링되는 경우입니다.

- 본문의 `{{ … }}`·`{% … %}`(프롬프트 템플릿, GitHub Actions의 `${{ … }}`): Jekyll이 글을
먼저 Liquid로 렌더링합니다. `{% raw %}…{% endraw %}`로 감쌉니다.
- 코드 밖의 `<think>`·`<s>` 같은 모델 토큰: 브라우저가 지웁니다. `코드`로 씁니다.
- `use_math: true` 없는 `$$` 수식, `\\(…\\)`로 쓴 수식, `$$` 안의 `\_`, 앞뒤 빈 줄이 없는
디스플레이 `$$` 줄.
- 마커 뒤에 언어 말고 다른 것이 붙은 코드 펜스, 닫히지 않은 펜스.
- 게시일로 시작하지 않는 파일명, 그보다 늦은 `date:`.
- ar5iv 링크: ar5iv는 잃어버린 그림 대신 대체 이미지를 HTTP 200으로 돌려줍니다.
`arxiv.org/html/<id>v1/…`를 쓰거나 그림을 `assets/images/`에 복사합니다.

### 푸시 전에 확인하세요

Expand Down
73 changes: 32 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ js/ main.js (theme toggle, code-copy, TOC, menu, image zoom…)
assets/images/ Topic cover images, plus post figures and diagrams (flat)
assets/<slug>/ Figures of some older posts; new posts use assets/images/
search.json Full-text search index (consumed by simple-jekyll-search)
test/ minitest unit tests for the _plugins/ logic
test/ minitest unit tests for _plugins/ and the post lint
script/ lint-posts.rb (post sources vs the conventions, before the build)
validate-site.sh (post-build discoverability checks)
check-plugin-output.rb (each plugin hook's mark on the posts;
Expand All @@ -111,8 +111,9 @@ no source file for any of them.

## Writing a post

The easiest path is the `/write-post` skill, which runs the whole
research → draft → proofread workflow. To add one by hand, create
Paper reviews come from scholar-lens as pull requests; the `/write-post` skill
(local, under the gitignored `.claude/`) runs research → draft → proofread for
the rest. To add one by hand, create
`_posts/YYYY-MM-DD-slug.md` starting with this front matter:

```yaml
Expand All @@ -121,13 +122,13 @@ layout: post
title: "<Post Title>"
subtitle: "<one-line pitch>" # optional — shown under the title in the header
date: YYYY-MM-DD HH:MM:SS
paper_author: "<Org>" # the paper's org, not people; omit for Insights/opinion posts
paper_author: "<Org>" # the paper's org, not people; paper posts only
seo_title: "<shorter title>" # optional — for <title> when the paper title is too long
description: >- # optional, plain text — see below
<search-snippet, ~150 chars>
categories: ["<Type>", "<Topic>"]
tags: ["<Tag-1>", "<Tag-2>"]
cover: /assets/images/<topic>.(jpg|png) # Insights: insights.jpg
cover: /assets/images/<topic>.(jpg|png) # the type's or topic's image; see below
use_math: true # ONLY if the post has equations (loads MathJax)
lang: ko # required with translation_id (ko | en)…
translation_id: <shared-slug> # …links a Korean post to its -en twin;
Expand All @@ -151,17 +152,9 @@ 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. They ship as plain text, so the lint rejects `$$`, `*emphasis*`,
backticks, links and tags in them.

### Dates, file names and Liquid

The file name starts with the posting day (`YYYY-MM-DD-`); `date:` may be earlier
(a paper's date) but not later. Jekyll renders every post as Liquid first, so a
`{{ … }}` or `{% … %}` in a code block (a prompt template, a GitHub Actions
`${{ … }}`) renders as nothing, or stops the build: wrap it in `{% raw %}…{% endraw %}`. The lint
checks all three.
Descriptions must be **unique across the site** and longer than the page title;
CI fails the build otherwise. They ship as plain text, so the lint rejects
`$$`, `*emphasis*`, backticks, links and tags in them.

### Categories drive the URL

Expand All @@ -172,9 +165,10 @@ Categories are **two levels**:
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**, one of the `topics` in
`_data/post_conventions.yml`, which also gives its cover image if it has one (Insights
posts use the type's `insights.jpg` instead). When none fits,
add one there first; the lint rejects any other value.
`_data/post_conventions.yml`, which also gives its cover image if it has one.
Insights posts use the type's `insights.jpg`; a topic without an image takes any
existing one. When no topic fits, add one there first; the lint rejects any
other value.

Jekyll slugifies the two and combines them with the date to build the output
path (`permalink` in `_config.yml`):
Expand Down Expand Up @@ -226,28 +220,25 @@ MathJax runs — e.g. `$a*b*c$` becomes `$a<em>b</em>c$` and renders broken. Wit
`$$`, kramdown emits verbatim `\(…\)` and leaves the contents alone. (Prose
dollar signs like `$10M` are fine — they're not math.)

The lint also rejects what renders wrong:

- math written `\\(…\\)` or `\\[…\\]`, or TeX next to a single `$`: Markdown
reads the formula first, as above.
- `\_` inside `$$…$$`: MathJax renders a literal underscore, not a subscript. Write
`_`; escape it only inside `\text{…}`.
- a display `$$…$$` line without a blank line before and after, in lists too:
kramdown folds it into the paragraph as inline math.
- Hangul inside a `$$` span: almost always two currency `$$`s read as one span.
- `$$` math in a post without `use_math: true`: MathJax never loads, so the TeX
shows as text.
- a `<name>` that is not HTML, `<s>`/`<input>`, or a bare `<img>` (a model's special token):
kramdown passes it through and the browser drops it. Write it in `code`.
- a code fence with more than a language after the marker (```` ```python title="a" ````),
or one never closed: kramdown does not read it as a fence.
- a tag spelled two ways across posts (`Grouped-Query-…` and `Grouped-query-…`).
- a `|` inside `$$` in a table row: kramdown splits the cell there, and turns
`\|` into `|` (a norm into an absolute value). Write `\lVert … \rVert`.

Inside inline math, write `\lvert … \rvert` (or `\mid`) instead of a bare `|`, and
`\lVert … \rVert` for a norm;
the lint catches it only in tables.
Inside math, write `\lvert … \rvert` or `\mid` for a bar and `\lVert … \rVert`
for a norm, never a bare `|`: in a table row kramdown splits the cell there.

### What else the lint catches

`ruby script/lint-posts.rb` names the line and the fix for each finding. The
rules easiest to trip, each a page that renders wrong with every other gate green:

- `{{ … }}` or `{% … %}` in the body (a prompt template, GitHub Actions'
`${{ … }}`): Jekyll renders posts as Liquid first. Wrap it in
`{% raw %}…{% endraw %}`.
- a model token such as `<think>` or `<s>` outside code: the browser drops it.
Write it in `code`.
- `$$` math without `use_math: true`, math written `\\(…\\)`, `\_` inside `$$`,
a display `$$` line without blank lines around it.
- a code fence with more than a language after the marker, or one never closed.
- a file name that does not start with the posting day, or a `date:` after it.
- an ar5iv link: ar5iv now answers a lost figure with a placeholder image and
HTTP 200. Use `arxiv.org/html/<id>v1/…`, or copy the figure into `assets/images/`.

### Validate before pushing

Expand Down
8 changes: 3 additions & 5 deletions _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@ exclude:
title: Bits, Bytes and Neural Networks
subtitle: A Data Scientist's Digital Playground
email: aldente0630@gmail.com
name: Jonas Kim
# jekyll-feed reads `author` (not `name`) for the Atom <author> element. Without
# it every entry ships an empty <author><name></name></author>. Posts never set
# `author` themselves — paper posts use `paper_author` — so this is every entry's.
# The site's author: jekyll-feed's Atom <author> and head.html's author meta and
# JSON-LD. Posts never set `author` themselves — paper posts use `paper_author`.
author: Jonas Kim
# The site's default language: the feed's xml:lang. Pages and posts still set
# their own `lang`; a post without one is in this language.
Expand All @@ -55,7 +53,7 @@ url: https://bits-bytes-nn.github.io
# afternoon-dated posts onto the next day and 404 their live URLs. Pinned to UTC
# because every published URL was built that way.
timezone: UTC
# Build posts dated later than the build. Posts here are dated 12:00 on their
# Build posts dated later than the build. Insights posts are dated 12:00 on their
# day (21:00 KST), so a same-day merge in the Korean morning would deploy
# without the post, and nothing rebuilds the site later. Nothing is scheduled.
future: true
Expand Down
14 changes: 6 additions & 8 deletions _includes/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@

<title>{{ html_title }}</title>
<meta name="description" content="{{ page_description | escape }}" />
{% if site.name %}
<meta name="author" content="{{ site.name }}" />
<meta name="copyright" content="&copy; {{ site.name }} {{ site.time | date: '%Y' }}" />
{% if site.author %}
<meta name="author" content="{{ site.author }}" />
<meta name="copyright" content="&copy; {{ site.author }} {{ site.time | date: '%Y' }}" />
{% endif %}

<!-- The hero banner is the largest thing on the first screen, but as a CSS
Expand All @@ -114,7 +114,6 @@
<link rel="manifest" href="{{ '/assets/icons/manifest.json' | prepend: site.baseurl }}">

<!-- Facebook OGP cards -->
{% if site.protocols.fb_image %}
<meta property="og:description" content="{{ page_description | escape }}" />
<meta property="og:url" content="{{ page_full_url }}" />
<meta property="og:site_name" content="{{ site.title }}" />
Expand All @@ -133,7 +132,6 @@
{% endif %} {% for cat in page.categories %}<meta property="article:section" content="{{ cat }}" />
{% endfor %}{% for tag in page.tags %}<meta property="article:tag" content="{{ tag | replace: '-', ' ' }}" />
{% endfor %}{% endif %}
{% endif %}

<!-- Twitter: card tags -->
<!-- Every image used here (page covers and the site default) is a wide
Expand Down Expand Up @@ -170,8 +168,8 @@
"dateModified": "{{ page.last_modified_at | default: page.date | date_to_xmlschema }}",
"description": {{ page_description | jsonify }},
"image": "{{ social_image }}",
"author": { "@type": "Person", "name": {{ site.name | jsonify }}, "url": "{{ '/about/' | prepend: site.baseurl | prepend: site.url }}" },
"publisher": { "@type": "Person", "name": {{ site.name | jsonify }} },
"author": { "@type": "Person", "name": {{ site.author | jsonify }}, "url": "{{ '/about/' | prepend: site.baseurl | prepend: site.url }}" },
"publisher": { "@type": "Person", "name": {{ site.author | jsonify }} },
"keywords": {{ page.tags | join: ', ' | replace: '-', ' ' | jsonify }}
}
</script>
Expand All @@ -185,7 +183,7 @@
"url": "{{ site.url }}{{ site.baseurl }}/",
"inLanguage": "{{ page_lang }}",
"description": {{ page_description | jsonify }},
"author": { "@type": "Person", "name": {{ site.name | jsonify }} }
"author": { "@type": "Person", "name": {{ site.author | jsonify }} }
}
</script>
{% endif %}
Expand Down
Loading