외부 프레임워크 없는 Python 표준 라이브러리 기반 정적 사이트 제너레이터(SSG) 설계Zero-Dependency Python Static Site Generator (SSG) Architecture
외부 패키지 의존성 없이 Python 표준 라이브러리(json, pathlib, html, re)만으로 JSON 콘텐츠를 렌더링하고, 마커 기반 부분 주입과 고아(Orphan) HTML 정리를 자동화하는 파이프라인 구조입니다.A static site build pipeline using only Python standard libraries (json, pathlib, html, re) to render JSON content, inject HTML via comment markers, and prune orphaned output files.
json, pathlib, html, re)만으로 정적 사이트 제너레이터(SSG)를 구축하면, 단일 스크립트 실행만으로 개별 문서 생성, 인덱스 페이지의 마커 구간(<!-- POSTS:START -->) 부분 교체, 삭제된 원본의 고아 HTML 정리까지 일관되게 처리할 수 있습니다.json, pathlib, html, re), a single script execution can deterministically handle individual page rendering, partial HTML injection between comment markers (<!-- POSTS:START -->), and automated cleanup of orphaned HTML files.1. 마커 기반 부분 주입 및 고아 파일 정리 파이프라인 구현1. Implementing Marker Injection and Orphan File Cleanup
정적 빌더의 핵심은 JSON 데이터를 HTML로 직렬화할 때 XSS 및 태그 깨짐을 방지하기 위해 html.escape를 거치고, 기존 레이아웃 파일의 특정 주석 마커 사이만 안전하게 교체하는 것입니다. 또한 슬러그가 변경되거나 삭제된 문서의 잔여 HTML 파일을 추적하여 제거합니다.
The core of the static builder is serializing JSON content into HTML via html.escape to prevent XSS and broken tags, while updating specific sections of index templates using comment markers. It also tracks and removes leftover HTML files when source slugs are renamed or deleted.
from __future__ import annotations
import html
import json
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
CONTENT_DIR = REPO_ROOT / "_content" / "posts"
OUTPUT_DIR = REPO_ROOT / "blog" / "posts"
BLOG_INDEX = REPO_ROOT / "blog" / "index.html"
POSTS_MARKERS = ("<!-- POSTS:START -->", "<!-- POSTS:END -->")
def inline_format(text: str) -> str:
"""HTML 이스케이프 후 인라인 마크다운(볼드, 코드, 링크)만 정규식으로 변환합니다."""
escaped = html.escape(str(text), quote=True)
escaped = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", escaped)
escaped = re.sub(r"`([^`]+)`", r"<code>\1</code>", escaped)
escaped = re.sub(
r"\[([^\]]+)\]\((https?://[^\)]+)\)",
r'<a href="\2" target="_blank" rel="noopener noreferrer">\1</a>',
escaped,
)
return escaped
def replace_between_markers(source_html: str, markers: tuple[str, str], payload: str) -> str:
"""HTML 내 주석 마커 사이의 구역을 새로 렌더링된 카드 목록으로 교체합니다."""
start_tag, end_tag = markers
if start_tag not in source_html or end_tag not in source_html:
return source_html
head, _, rest = source_html.partition(start_tag)
_, _, tail = rest.partition(end_tag)
return f"{head}{start_tag}\n{payload}\n {end_tag}{tail}"
def clean_orphan_outputs(valid_posts: list[dict]) -> None:
"""원본 JSON이 삭제되었으나 출력 폴더에 남아 있는 고아 HTML을 제거합니다."""
expected_files = {f"{p['date']}-{p['slug']}.html" for p in valid_posts}
for html_path in OUTPUT_DIR.glob("*.html"):
# 리다이렉트 스텁 파일은 삭제 대상에서 제외
head_chunk = html_path.read_text(encoding="utf-8", errors="replace")[:1024]
if 'http-equiv="refresh"' in head_chunk:
continue
if html_path.name not in expected_files:
html_path.unlink()
print(f"[DEL] 고아 HTML 제거: {html_path.name}")
from __future__ import annotations
import html
import json
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
CONTENT_DIR = REPO_ROOT / "_content" / "posts"
OUTPUT_DIR = REPO_ROOT / "blog" / "posts"
BLOG_INDEX = REPO_ROOT / "blog" / "index.html"
POSTS_MARKERS = ("<!-- POSTS:START -->", "<!-- POSTS:END -->")
def inline_format(text: str) -> str:
"""Escape HTML first, then convert inline markdown (bold, code, links) via regex."""
escaped = html.escape(str(text), quote=True)
escaped = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", escaped)
escaped = re.sub(r"`([^`]+)`", r"<code>\1</code>", escaped)
escaped = re.sub(
r"\[([^\]]+)\]\((https?://[^\)]+)\)",
r'<a href="\2" target="_blank" rel="noopener noreferrer">\1</a>',
escaped,
)
return escaped
def replace_between_markers(source_html: str, markers: tuple[str, str], payload: str) -> str:
"""Replace the section between HTML comment markers with newly rendered cards."""
start_tag, end_tag = markers
if start_tag not in source_html or end_tag not in source_html:
return source_html
head, _, rest = source_html.partition(start_tag)
_, _, tail = rest.partition(end_tag)
return f"{head}{start_tag}\n{payload}\n {end_tag}{tail}"
def clean_orphan_outputs(valid_posts: list[dict]) -> None:
"""Remove orphaned HTML files in the output directory whose source JSON was deleted."""
expected_files = {f"{p['date']}-{p['slug']}.html" for p in valid_posts}
for html_path in OUTPUT_DIR.glob("*.html"):
# Exclude redirect stub files from deletion
head_chunk = html_path.read_text(encoding="utf-8", errors="replace")[:1024]
if 'http-equiv="refresh"' in head_chunk:
continue
if html_path.name not in expected_files:
html_path.unlink()
print(f"[DEL] Removed orphan HTML: {html_path.name}")
2. SSG 파이프라인 핵심 모듈 및 역할2. Core SSG Modules and Responsibilities
| 모듈 / 함수 | 사용 표준 라이브러리 | 핵심 동작 및 설계 목적 |
|---|---|---|
load_posts() | pathlib, json | _content/ 하위 JSON 파싱 및 <date>-<slug>.json 정본 규칙 기반 중복 URL 필터링 |
inline_format() | html, re | 원본 문자열을 html.escape()로 먼저 이스케이프한 뒤 제한된 인라인 문법만 태그로 치환 |
replace_between_markers() | str.partition | 정규식 백트래킹 비용 없이 <!-- POSTS:START -->와 END 마커 사이 DOM 조각만 정밀 교체 |
clean_orphan_outputs() | pathlib.Path.unlink | 원본 JSON에서 제외된 옛 슬러그 HTML을 삭제하되 <meta http-equiv="refresh"> 스텁은 보존 |
| Module / Function | Standard Library Used | Core Behavior and Purpose |
|---|---|---|
load_posts() | pathlib, json | Parses JSON files under _content/ and filters duplicate URLs using the <date>-<slug>.json naming rule |
inline_format() | html, re | Escapes raw strings via html.escape() before converting inline markdown syntax into HTML tags |
replace_between_markers() | str.partition | Replaces DOM fragments between <!-- POSTS:START --> and END markers without regex backtracking overhead |
clean_orphan_outputs() | pathlib.Path.unlink | Deletes stale HTML files lacking source JSON while preserving <meta http-equiv="refresh"> redirect stubs |
3. 실무 적용 및 주의사항3. Practical Considerations and Caveats
- 이스케이프 순서 준수: 인라인 마크다운 치환 전에 반드시
html.escape()를 먼저 호출해야 본문 내<script>나 제네릭 타입 표기(List<T>)가 깨지지 않고 안전하게 렌더링됩니다. - 운영체제별 표준 입출력 인코딩 고정: Windows 환경에서 파이썬 스크립트를 실행할 때
cp949인코딩 오류가 발생하지 않도록 파일 읽기/쓰기에encoding="utf-8"을 명시하고sys.stdout.reconfigure(encoding="utf-8")를 선언합니다. - 리다이렉트 스텁 보호: URL 구조 변경 후 검색엔진 최적화를 위해 남겨둔 리다이렉트용 HTML 파일이 고아 정리 루틴에 의해 삭제되지 않도록 파일 상단 메타 태그를 검사하는 예외 처리가 필요합니다.
- Strict Escaping Order: Always call
html.escape()prior to regex tag substitution so that raw<script>tags or generic type notations (List<T>) in prose render safely without breaking the DOM. - Explicit UTF-8 I/O on Windows: Specify
encoding="utf-8"on all file operations and callsys.stdout.reconfigure(encoding="utf-8")to avoidcp949codec errors on Windows terminals. - Preserving Redirect Stubs: When cleaning orphaned files, inspect the file header for
<meta http-equiv="refresh">so intentional redirect stubs for migrated URLs are not deleted.
외부 의존성을 배제한 표준 라이브러리 기반 SSG 구조는 런타임 버전 변화에 영향을 받지 않으며, 수백 개의 문서와 블로그 포스트를 수 초 내에 일관된 정적 HTML로 빌드할 수 있게 해줍니다.
Building an SSG strictly on top of the Python standard library eliminates external package maintenance overhead and compiles hundreds of articles into static HTML within seconds.