예제
KO

버전별 문서

Hwaro는 하나의 사이트에서 같은 문서의 여러 버전(v1, v2, …)을 게시할 수 있습니다. 버전 전환기, 버전별 내비게이션, 그리고 검색 엔진을 최신 릴리스로 안내하는 SEO를 제공합니다. 버전 관리는 디렉터리 기반이며 다국어 지원을 본떠 설계되었습니다. 페이지는 자신을 담고 있는 콘텐츠 디렉터리의 버전에 속합니다.

설정

[versions]
latest_at_root = true     # 최신 버전은 섹션의 원래 URL에 렌더링
noindex_old = true        # 이전 버전: <meta name="robots" content="noindex"> + 최신 대응 페이지로 canonical
search = "latest"         # "latest" | "all" — search.json과 sitemap.xml에 들어갈 버전
feeds = "latest"          # "latest" | "all" — RSS/Atom에 들어갈 버전
taxonomies = "latest"     # "latest" | "all" — 택소노미 용어 페이지가 수집할 버전

[[versions.list]]
name = "v2"               # URL 세그먼트, URL-safe 문자만 허용 (영문자, 숫자, - _ . ~)
label = "2.x (latest)"    # 전환기 라벨 (기본값: name)
path = "docs/v2"          # content/ 기준 콘텐츠 디렉터리 (기본값: name)
latest = true

[[versions.list]]
name = "v1"
label = "1.x"
path = "docs/v1"

TOML에서는 하나의 키가 테이블과 테이블 배열을 동시에 가질 수 없으므로, 스위치는 [versions]에, 항목은 [[versions.list]]에 둡니다. 기본값만 필요하다면 배열만 써도 됩니다:

[[versions]]
name = "v2"
path = "docs/v2"
latest = true

[[versions]]
name = "v1"
path = "docs/v1"

검증 규칙 (모두 HWARO_E_CONFIG로 빌드가 실패합니다):

hwaro doctor는 존재하지 않는 콘텐츠 디렉터리를 가리키는 버전이 있으면 version-path-missing 경고를 냅니다.

콘텐츠 구조

각 버전은 자기 디렉터리 아래의 일반 콘텐츠 트리입니다. 버전 루트 기준 상대 경로가 같은 파일은 다른 버전의 같은 페이지로 취급되며, 전환기는 이 규칙으로 대응 페이지를 찾습니다.

content/
└── docs/
    ├── v2/
    │   ├── _index.md
    │   ├── install.md
    │   └── plugins.md      # v2에만 존재
    └── v1/
        ├── _index.md
        ├── install.md
        └── legacy.md       # v1에만 존재

URL 매핑

버전 디렉터리는 그 버전이 게시되는 디렉터리로 치환됩니다. latest_at_root = true(기본값)이면 최신 버전이 상위 디렉터리의 원래 URL을 차지하고, 이전 버전은 /<name>/ 세그먼트를 갖습니다:

소스 URL
content/docs/v2/_index.md /docs/
content/docs/v2/install.md /docs/install/
content/docs/v1/_index.md /docs/v1/
content/docs/v1/install.md /docs/v1/install/

latest_at_root = false이면 모든 버전이 세그먼트를 유지하고(/docs/v2/install/, /docs/v1/install/), /docs/는 최신 버전 루트로 향하는 리디렉션 스텁이 됩니다. 직접 content/docs/_index.md를 작성했다면 그 페이지가 URL을 유지합니다.

URL 세그먼트는 디렉터리 이름이 아니라 버전 name입니다. name = "2.x", path = "docs/v2"라면 루트가 아닐 때 /docs/2.x/…에 게시됩니다. 버전 디렉터리는 최상위(content/v2/…)에 둘 수도 있으며, 이 경우 최신 버전이 사이트 루트가 됩니다.

참고:

다국어

언어와 버전은 함께 사용할 수 있습니다. 언어 접두어가 먼저, 버전이 그 뒤에 옵니다. content/docs/v1/install.ko.md/ko/docs/v1/install/에 렌더링되며, 버전 디렉터리 안의 foo.ko.md 파일은 다른 곳과 똑같이 동작합니다(번역 연결, hreflang, 언어별 메뉴).

템플릿 변수

page.version

버전이 없는 페이지에서는 nil이므로({% if page.version %}이 가드), 그 외에는:

속성 타입 설명
.name String 버전 이름 ("v2")
.label String 표시 라벨 ("2.x (latest)")
.latest Bool 최신 버전인지
.url String 페이지 언어 기준 버전 루트 URL (/docs/, /ko/docs/v1/)

설정된 버전마다 하나씩, 설정 순서대로 나열되며, 전환기의 각 행이 됩니다. 버전이 없는 페이지에서는 비어 있습니다.

속성 타입 설명
.name String 버전 이름
.label String 표시 라벨
.latest Bool 최신 버전인지
.url String 그 버전에 같은 페이지가 있으면 그 URL, 없으면 그 버전의 루트
.exists Bool 대응 페이지 존재 여부 (falseurl은 버전 루트)
.current Bool 이 행이 현재 페이지의 버전인지

대응 페이지는 같은 언어에서 버전 루트 기준 상대 경로로 매칭됩니다: docs/v1/install.mddocs/v2/install.md, docs/v1/install.ko.mddocs/v2/install.ko.md. render = false인 대응 페이지는 존재하지 않는 것으로 취급합니다.

versions (전역)

버전 사이트의 모든 페이지에서 사용할 수 있습니다. {name, label, latest, url} 항목의 리스트({% for v in versions %})이며 url은 현재 페이지 언어 기준 각 버전의 루트입니다. 추가로:

속성 설명
versions.latest 최신 버전 항목
versions.all 같은 리스트를 일반 배열로
versions.size 버전 개수

page.url처럼 위의 모든 URL은 사이트 상대 경로입니다. 서브패스 배포가 동작하도록 링크에는 {{ base_url }}(또는 {{ base_path }})을 앞에 붙이세요.

버전 전환기 예시

{% if page.version %}
<details class="version-switch">
  <summary aria-label="문서 버전 전환">
    {{ page.version.label }}
    {% if not page.version.latest %}<span class="badge">old</span>{% endif %}
  </summary>
  <ul>
    {% for v in page.version_links %}
    <li>
      <a href="{{ base_url }}{{ v.url }}"
         {% if v.current %}class="current" aria-current="page"{% endif %}
         {% if not v.exists %}title="이 페이지는 {{ v.label }}에 없습니다 — {{ v.label }} 시작 페이지로 이동"{% endif %}>
        {{ v.label }}{% if v.latest %} (latest){% endif %}
      </a>
    </li>
    {% endfor %}
  </ul>
</details>

{% if not page.version.latest %}
<div class="version-banner">
  {{ page.version.label }} 문서를 보고 있습니다.
  {% for v in page.version_links %}{% if v.latest %}
  <a href="{{ base_url }}{{ v.url }}">{% if v.exists %}{{ v.label }}에서 이 페이지 보기{% else %}{{ v.label }} 문서로 이동{% endif %}</a>
  {% endif %}{% endfor %}
</div>
{% endif %}
{% endif %}

현재 페이지에 의존하지 않는 사이트 전역 진입점:

<a href="{{ base_url }}{{ versions.latest.url }}">Docs ({{ versions.latest.label }})</a>
<select onchange="location.href=this.value">
  {% for v in versions %}
  <option value="{{ base_url }}{{ v.url }}">{{ v.label }}</option>
  {% endfor %}
</select>

범위 규칙

각 버전은 독립된 트리이며, 경계를 넘어 새는 것은 없습니다:

대상 동작
page.lower / page.higher 읽기 순서는 {언어, 버전}별로 만들어집니다. 마지막 v1 페이지에는 "다음"이 없고 v2로 넘어가지 않습니다.
page.ancestors (브레드크럼) 버전 루트에서 멈춥니다. 버전 없는 docs/_index.mddocs/v1/…의 조상이 아닙니다.
get_section(), section.pages, section.subsections 버전 루트는 버전 없는 상위 섹션의 하위 섹션이나 페이지가 아니며, 버전 안의 목록은 그 버전만 봅니다.
메뉴 (get_menu) 설정의 [[menus.*]] 항목은 어디에나 나타납니다. 버전 페이지의 프론트 매터 menus = […] 등록은 그 버전의 메뉴에만 나타나고, 버전 없는 페이지는 최신 버전의 등록을 추가로 봅니다.
관련 글 버전 경계를 넘지 않습니다.
택소노미 용어 페이지는 버전 없는 콘텐츠와 최신 버전에서 수집합니다(taxonomies = "latest", 기본값). 모든 버전을 나열하려면 taxonomies = "all".

시리즈는 버전을 인식하지 않습니다. v1 페이지와 v2 페이지가 같은 series를 쓰면 함께 묶입니다.

SEO와 탐색 표면

Canonical과 noindex

이전 버전의 페이지는 최신 대응 페이지가 있으면 그쪽으로 canonical 링크를 내고(없으면 자기 자신), noindex_old = true(기본값)이면 바로 뒤에 <meta name="robots" content="noindex">를 붙입니다. 둘 다 {{ canonical_tag }}에서 나오므로 이미 이 변수를 출력하는 템플릿은 수정이 필요 없습니다. seo.canonical_url도 같은 규칙을 따르고 seo.noindex가 플래그를 노출합니다.

<link rel="canonical" href="https://example.com/docs/install/">
<meta name="robots" content="noindex">

최신 버전 페이지는 평소처럼 자기 자신을 canonical로 가리킵니다. 페이지네이션된 목록은 계속 자기 자신을 가리킵니다(이전 섹션의 page/2/는 새 섹션의 page/2/가 아닙니다). hreflang_tags는 영향을 받지 않으며 같은 버전의 번역본을 연결합니다.

탐색 표면

표면 스위치 기본값
search.json [versions] search 최신만
sitemap.xml [versions] search (같은 스위치) 최신만
RSS / Atom (메인, 섹션, 언어별) [versions] feeds 최신만
택소노미 용어 페이지 [versions] taxonomies 최신만
llms.txt / llms-full.txt 항상 최신만

버전 없는 페이지는 항상 포함됩니다. search = "all"이면 search.json의 모든 레코드에 version 필드(버전 이름)가 들어가므로 클라이언트에서 읽고 있는 버전으로 결과를 필터링할 수 있습니다:

const current = document.documentElement.dataset.version; // 예: data-version="{{ page.version.name }}"
const hits = results.filter((r) => !r.version || r.version === current);

빌드 캐시, serve, doctor

포함되지 않은 것

참고