예제
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을 유지합니다. 다만 캐스케이드용으로 둔 헤드리스(render = false) 페이지는 기록되지 않으므로 스텁이 그대로 남습니다.

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 대응 페이지 존재 여부 (false면 url은 버전 루트)
.current Bool 이 행이 현재 페이지의 버전인지

대응 페이지는 같은 언어에서 버전 루트 기준 상대 경로로 매칭됩니다: docs/v1/install.md ↔ docs/v2/install.md, docs/v1/install.ko.md ↔ docs/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.md는 docs/v1/…의 조상이 아닙니다.
get_section(), section.pages, section.subsections 버전 루트는 버전 없는 상위 섹션의 하위 섹션이나 페이지가 아니며, 버전 안의 목록은 그 버전만 봅니다.
메뉴 (get_menu) 설정의 [[menus.*]] 항목은 어디에나 나타납니다. 버전 페이지의 프론트 매터 menus = […] 등록은 그 버전의 메뉴에만 나타나고, 버전 없는 페이지는 최신 버전의 등록을 추가로 봅니다.
관련 글 버전 경계를 넘지 않습니다.
시리즈 {언어, 버전}별로 묶입니다. v1 페이지와 v2 페이지가 같은 series를 쓰면 서로 다른 두 시리즈가 되며, 각 버전 안에서 따로 번호가 매겨집니다.
택소노미 용어 페이지는 버전 없는 콘텐츠와 최신 버전에서 수집합니다(taxonomies = "latest", 기본값). 모든 버전을 나열하려면 taxonomies = "all".

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

포함되지 않은 것

함께 보기