예제
KO

데이터 모델

Hwaro의 템플릿 시스템은 Site, Section, Page 세 가지 핵심 타입을 중심으로 동작합니다. 이 페이지는 템플릿 쪽 레퍼런스로, 템플릿을 만들 때 쓸 수 있는 모든 속성과 변수를 다룹니다. 콘텐츠 작성과 프론트 매터 필드 설정은 콘텐츠 작성을 참고합니다.

계층 구조

Site
├── Config (title, base_url, ...)
├── Pages[] (standalone pages)
├── Sections[]
│   ├── Pages[] (pages in section)
│   └── Subsections[]
│       ├── Pages[]
│       └── Subsections[] (recursive)
└── Taxonomies{}
    └── Terms{}
        └── Pages[]

관계

Site

최상위 컨테이너입니다. config.toml에서 설정합니다.

속성

속성 타입 설명
site.title String 사이트 제목
site.description String 사이트 설명
site.base_url String 기본 URL(끝 슬래시 없음)
site.pages Array<Page> 섹션에 속하지 않은 모든 페이지
site.sections Array<Section> 모든 섹션 인덱스 페이지
site.taxonomies Object 모든 택소노미 그룹과 항목
site.data Object data/ 디렉터리에서 불러온 데이터
site.authors Object 집계된 작성자 데이터
site.menus Object 기본 언어의 이름 있는 메뉴(메뉴 참고)

플랫 별칭

변수 동일 표현
site_title site.title
site_description site.description
base_url site.base_url

데이터 디렉터리

보조 데이터는 data/ 디렉터리에 둡니다. .yml, .yaml, .json, .toml로 끝나는 파일은 자동으로 로드되어 site.data로 노출됩니다.

파일 구조

data/
├── authors.yml
├── products.json
├── config.toml
└── users/
    ├── alice.yml
    ├── bob.yml
    └── cho.yml

데이터 접근

데이터는 확장자를 뺀 파일 이름으로 접근합니다.

예를 들어 data/products.json이 다음과 같다면:

[
  {"name": "Widget", "price": 10},
  {"name": "Gadget", "price": 20}
]

템플릿에서 이렇게 접근합니다.

{% for product in site.data.products %}
  <h2>{{ product.name }}</h2>
  <p>{{ product.price }}</p>
{% endfor %}

하위 디렉터리

data/ 아래의 하위 디렉터리는 중첩된 맵이 됩니다. 각 파일은 확장자를 뺀 파일 이름을 키로 하는 자식이 되고, 부모 디렉터리 자체는 순회할 수 있습니다.

위 구조에서 data/users/alice.yml, data/users/bob.yml, data/users/cho.yml은 다음과 같이 노출됩니다.

{% for name, user in site.data.users %}
  <h3>{{ name }}</h3>
  <p>{{ user.bio }}</p>
{% endfor %}

디렉터리는 얼마든지 중첩됩니다: data/users/admins/root.ymlsite.data.users.admins.root.

충돌. 디렉터리와 파일이 같은 이름을 공유하면(예: data/users.ymldata/users/가 나란히 있는 경우) 디렉터리가 우선하고 파일은 무시됩니다. 가려진 파일이 조용히 사라지지 않도록 Hwaro는 빌드 중에 경고를 출력합니다.

사이트 작성자

Hwaro는 프론트 매터의 authors 필드(authors = ["id"])에 정의된 모든 작성자를 site.authors로 자동 집계합니다.

작성자 정의

data/authors.yml(또는 .json, .toml) 파일을 만들어 작성자 데이터를 보강할 수 있습니다. 키는 페이지 프론트 매터에서 쓴 작성자 ID와 일치해야 합니다.

content/my-post.md

---
title: "My Post"
authors: ["john-doe"]
---

data/authors.yml

john-doe:
  name: "John Doe"
  bio: "Creator of things."
  avatar: "/images/john.jpg"

템플릿에서 사용

site.authors 객체는 사이트에서 발견된 모든 작성자를 담습니다. 각 작성자 객체는 다음을 갖습니다.

{% for id, author in site.authors %}
  <div class="author">
    <img src="{{ author.avatar }}" alt="{{ author.name }}">
    <h3>{{ author.name }}</h3>
    <p>{{ author.bio }}</p>

    <h4>Recent Posts</h4>
    <ul>
    {% for p in author.pages %}
      <li><a href="{{ p.url }}">{{ p.title }}</a></li>
    {% endfor %}
    </ul>
  </div>
{% endfor %}

예시

<title>{{ site.title }}</title>
<link rel="canonical" href="{{ site.base_url }}{{ page.url }}">

Section

관련 콘텐츠를 묶는, _index.md가 있는 디렉터리입니다.

속성

속성 타입 설명
section.title String 섹션 제목
section.description String? 섹션 설명
section.pages Array<Page> 이 섹션의 페이지
section.pages_count Int 페이지 수
section.list String 미리 렌더링된 HTML 목록(section_list)
section.subsections Array<Section> 자식 섹션
section.assets Array<String> 섹션의 정적 파일
section.page_template String? 페이지 기본 템플릿
section.paginate_path String 페이지네이션 URL 패턴
section.redirect_to String? 리다이렉트 URL

section.html에서 현재 섹션의 URL이 필요하면 page.url을 사용합니다.

플랫 별칭

변수 동일 표현
section_title section.title
section_description section.description
section_list 미리 렌더링된 페이지 HTML 목록

프론트 매터에서 오는 속성

속성 타입 기본값 설명
sort_by String? "date" 정렬 기준: date(최신순), weight(낮은 값 우선), title(A→Z)
reverse Bool? false 기본 정렬 순서 뒤집기 — 콘텐츠 작성 › 섹션 › 정렬 방향 참고
paginate Int? 한 페이지에 표시할 페이지 수
transparent Bool false 페이지를 부모 섹션으로 전달
generate_feeds Bool false RSS 피드 생성

페이지 순회

{% for p in section.pages %}
<article>
  <h2><a href="{{ p.url }}">{{ p.title }}</a></h2>
  <time>{{ p.date }}</time>
  {% if p.description %}
  <p>{{ p.description }}</p>
  {% endif %}
</article>
{% endfor %}

하위 섹션 순회

{% for sub in section.subsections %}
<div class="category">
  <a href="{{ sub.url }}">{{ sub.title }}</a>
  <span>({{ sub.pages_count }} articles)</span>
</div>
{% endfor %}

section_list 사용

단순한 목록에는 미리 렌더링된 HTML을 사용합니다.

<ul>{{ section_list | safe }}</ul>

마크업을 직접 구성하려면 section.pages를 순회합니다.

<ul>
{% for p in section.pages %}
  <li>
    <a href="{{ p.url }}">{{ p.title }}</a>
    {% if p.date %}<time>{{ p.date }}</time>{% endif %}
  </li>
{% endfor %}
</ul>

Page

개별 콘텐츠 파일(.md)입니다.

핵심 속성

속성 타입 설명
page.title String 페이지 제목
page.description String? 페이지 설명
page.url String 상대 URL 경로
page.permalink String? base_url을 포함한 절대 URL
page.section String 부모 섹션 이름
page.date String? 발행일(YYYY-MM-DD)
page.updated String? 마지막 수정일
page.language String 적용된 언어 코드
page.translations Array<TranslationLink> 언어별 번역본

렌더링된 HTML 콘텐츠는 최상위 content 변수로 제공됩니다.

메타데이터 속성

속성 타입 설명
page.draft Bool 초안 여부
page.weight Int 정렬 가중치
page.image String? 대표 이미지 경로
page.authors Array<String> 작성자 이름
page.taxonomies Object 이 페이지의 택소노미 항목(page.taxonomies.tags, page.taxonomies.<name>)
page.extra Object 커스텀 프론트 매터 필드

계산 속성

속성 타입 설명
page.word_count Int 단어 수
page.reading_time Int 읽기 시간(분)
page.summary String? <!-- more --> 앞부분을 렌더링한 HTML. 마커가 없으면 page.description으로 폴백. 삽입할 때는 | safe와 함께 사용(예: {{ page.summary | safe }}). <meta name="description">에는 page.description을 직접 사용
page.assets Array<String> 페이지 번들의 정적 파일
page.series String 프론트 매터의 시리즈 이름(없으면 빈 문자열)
page.series_index Int 시리즈 안에서 1부터 시작하는 순번([series] 활성화 필요)
page.series_pages Array<Page> 같은 시리즈의 모든 페이지(series_weight 순 정렬)
page.related_posts Array<Page> 택소노미 항목을 공유하는 페이지([related] 활성화 필요)

불리언 플래그

속성 타입 기본값 설명
page.toc Bool false 목차 표시 여부
page.render Bool true 렌더링 여부
page.is_index Bool 인덱스 파일 여부
page.generated Bool false 자동 생성된 페이지 여부
page.in_sitemap Bool true 사이트맵 포함 여부
page.in_search_index Bool true 검색 인덱스 포함 여부

내비게이션 속성

속성 타입 설명
page.lower Page? 읽기 순서상 이전 페이지
page.higher Page? 읽기 순서상 다음 페이지
page.ancestors Array<Page> 부모 섹션 체인
page.translations Array<TranslationLink> 언어별 번역본

커스텀 메타데이터

속성 타입 설명
page.extra Object 커스텀 프론트 매터 필드

플랫 별칭

변수 동일 표현
page_title page.title
page_description page.description
page_url page.url
page_section page.section
page_date page.date
page_image page.image
page_summary page.summary
page_word_count page.word_count
page_reading_time page.reading_time
page_permalink page.permalink
page_authors page.authors
page_weight page.weight
page_language page.language
page_translations page.translations
taxonomy_name 현재 택소노미 이름(택소노미 페이지)
taxonomy_term 현재 택소노미 항목(택소노미 항목 페이지)
content 렌더링된 HTML 콘텐츠

내비게이션 객체

page.lower / page.higher

내비게이션은 mdBook이나 Docusaurus처럼 사이트 전체를 하나로 펼친 읽기 순서를 따릅니다. 페이지는 섹션 트리를 깊이 우선으로 순회하는 순서로 정렬됩니다: 섹션 인덱스 → 섹션 페이지 → 하위 섹션(재귀). 각 섹션 안에서는 그 섹션의 sort_by 설정(weight, date, title)에 따라 정렬됩니다.

속성 타입 설명
.title String 페이지 제목
.url String 페이지 URL
.description String? 페이지 설명
.date String? 페이지 날짜
<nav class="post-nav">
  {% if page.lower %}
  <a href="{{ page.lower.url }}">← {{ page.lower.title }}</a>
  {% endif %}
  
  {% if page.higher %}
  <a href="{{ page.higher.url }}">{{ page.higher.title }} →</a>
  {% endif %}
</nav>

page.ancestors

브레드크럼에 쓰는 부모 섹션 목록:

<nav class="breadcrumbs">
  <a href="/">Home</a>
  {% for ancestor in page.ancestors %}
  / <a href="{{ ancestor.url }}">{{ ancestor.title }}</a>
  {% endfor %}
  / <span>{{ page.title }}</span>
</nav>

page.translations

속성 타입 설명
.code String 언어 코드(예: "en")
.url String 번역된 페이지 URL
.title String 해당 언어의 제목
.is_current Bool 현재 페이지의 언어인지
.is_default Bool 기본 언어인지
{% if page.translations %}
<nav class="lang-switcher">
{% for t in page.translations %}
  {% if t.is_current %}
  <span>{{ t.code | upper }}</span>
  {% else %}
  <a href="{{ t.url }}">{{ t.code | upper }}</a>
  {% endif %}
{% endfor %}
</nav>
{% endif %}

page.extra 접근

프론트 매터의 커스텀 메타데이터:

+++
title = "Review"

[extra]
rating = 4.5
featured = true
pros = ["Fast", "Reliable"]
+++
{% if page.extra.featured %}
<span class="badge">Featured</span>
{% endif %}

<div class="rating">{{ page.extra.rating }} / 5</div>

<ul>
{% for pro in page.extra.pros %}
  <li>{{ pro }}</li>
{% endfor %}
</ul>

프론트 매터 최상위에 쓴 outputs = ["json"]도 마찬가지로 알려지지 않은 일반 키로 취급되어 page.extra.outputs에 들어갑니다 — 해당 페이지/섹션에 한해 [outputs] 설정 기본값을 재정의합니다. 출력 포맷을 참고합니다.


시간 변수

변수 타입 설명
current_year Int 현재 연도(예: 2025)
current_date String 현재 날짜(YYYY-MM-DD)
current_datetime String 현재 날짜와 시간
<footer>&copy; {{ current_year }} {{ site.title }}</footer>

SEO 변수

미리 렌더링된 HTML(하위 호환):

변수 설명
og_tags OpenGraph 메타 태그
twitter_tags Twitter Card 메타 태그
og_all_tags OG와 Twitter 태그 전부
canonical_tag 캐노니컬 링크 태그
hreflang_tags Hreflang 대체 링크 태그(다국어)
pagination_seo_links <link rel="prev/next"> 태그
<head>
  {{ og_all_tags | safe }}
  {{ canonical_tag | safe }}
  {{ hreflang_tags | safe }}
  {{ pagination_seo_links | safe }}
</head>

메타 태그 마크업을 직접 구성할 때 쓰는 구조화된 데이터:

속성 타입 설명
seo.canonical_url String 전체 캐노니컬 URL(base_url + 페이지 URL)
seo.og_type String OpenGraph 타입(기본: "article")
seo.og_image String 절대 경로로 해석된 이미지 URL
seo.twitter_card String Twitter 카드 타입(기본: "summary_large_image")
seo.twitter_site String Twitter 사이트 핸들
seo.twitter_creator String Twitter 작성자 핸들
seo.fb_app_id String Facebook 앱 ID
seo.hreflang Array page.translations와 동일

페이지 제목, 설명, URL, 이미지는 page.title, page.description, page.url, page.image로 얻습니다. seo 객체는 SEO에 특화된 계산 값(해석된 URL, 설정 값)을 제공합니다.

<head>
  <link rel="canonical" href="{{ seo.canonical_url }}">
  <meta property="og:title" content="{{ page.title }}">
  <meta property="og:type" content="{{ seo.og_type }}">
  <meta property="og:url" content="{{ seo.canonical_url }}">
  {% if page.description %}
  <meta property="og:description" content="{{ page.description }}">
  {% endif %}
  {% if seo.og_image %}
  <meta property="og:image" content="{{ seo.og_image }}">
  {% endif %}
  <meta name="twitter:card" content="{{ seo.twitter_card }}">
  {% if seo.twitter_site %}
  <meta name="twitter:site" content="{{ seo.twitter_site }}">
  {% endif %}
</head>

에셋 변수

편의를 위해 미리 렌더링된 <link>·<script> 태그입니다. config.toml 설정에서 생성됩니다.

변수 설명
highlight_css 구문 강조 CSS <link> 태그
highlight_js 구문 강조 JS <script> 태그
highlight_tags CSS와 JS 태그 전부
auto_includes_css 자동 인클루드된 CSS <link> 태그
auto_includes_js 자동 인클루드된 JS <script> 태그
auto_includes 모든 자동 인클루드 태그
<head>
  {{ highlight_css | safe }}
  {{ auto_includes_css | safe }}
</head>
<body>
  ...
  {{ highlight_js | safe }}
  {{ auto_includes_js | safe }}
</body>

목차

프론트 매터에 toc = true가 있을 때만 사용할 수 있습니다.

미리 렌더링된 HTML(하위 호환):

변수 타입 설명
toc String 생성된 목차 HTML
toc_obj.html String 같은 목차 HTML의 객체 형태
{% if page.toc %}
<aside class="toc">
  {{ toc | safe }}
</aside>
{% endif %}

목차 마크업을 직접 구성할 때 쓰는 구조화된 데이터:

속성 타입 설명
toc_obj.headers Array 구조화된 목차 헤더 객체
toc_obj.headers[].level Int 헤딩 레벨(2-6)
toc_obj.headers[].id String 앵커 ID
toc_obj.headers[].title String 헤딩 텍스트
toc_obj.headers[].permalink String 전체 앵커 퍼머링크
toc_obj.headers[].children Array 중첩된 자식 헤더(같은 구조)
{% if page.toc %}
<nav class="toc">
  <ul>
  {% for h in toc_obj.headers %}
    <li>
      <a href="{{ h.permalink }}">{{ h.title }}</a>
      {% if h.children %}
      <ul>
        {% for child in h.children %}
        <li><a href="{{ child.permalink }}">{{ child.title }}</a></li>
        {% endfor %}
      </ul>
      {% endif %}
    </li>
  {% endfor %}
  </ul>
</nav>
{% endif %}

Paginator

섹션·택소노미 항목 템플릿에서 페이지네이션이 활성화된 경우(섹션 프론트 매터paginate, 택소노미는 paginate_by) 사용할 수 있습니다. 1페이지는 섹션 URL에, 이후 페이지는 {url}/{paginate_path}/{n}/에 생성됩니다.

속성 타입 설명
paginator.paginate_by Int 페이지당 항목 수
paginator.base_url String 페이저 기본 URL({url}/{paginate_path}/)
paginator.number_pagers Int 전체 페이지 수
paginator.first String 첫 페이지 URL
paginator.last String 마지막 페이지 URL
paginator.previous String? 이전 페이지 URL(첫 페이지에서는 nil)
paginator.next String? 다음 페이지 URL(마지막 페이지에서는 nil)
paginator.pages Array<Page> 현재 페이저의 페이지 목록
paginator.current_index Int 현재 페이지 번호(1부터 시작)
paginator.total_pages Int number_pagers와 동일

같은 데이터를 pagination_obj 변형으로도 쓸 수 있습니다. previous_url, next_url, first_url, last_url, current_page, total_pages, total_items, per_page, has_previous, has_next, 그리고 html(미리 렌더링된 내비게이션, 플랫 변수 pagination으로도 제공)을 노출합니다.

{% if paginator is defined and paginator.number_pagers > 1 %}
<nav>
  {% if paginator.previous %}<a href="{{ paginator.previous }}">Prev</a>{% endif %}
  <span>{{ paginator.current_index }} / {{ paginator.number_pagers }}</span>
  {% if paginator.next %}<a href="{{ paginator.next }}">Next</a>{% endif %}
</nav>
{% endif %}

택소노미 변수

택소노미 템플릿에서 사용할 수 있습니다.

변수 타입 설명
taxonomy_name String 택소노미 이름(예: "tags")
taxonomy_term String 현재 항목 이름(인덱스 페이지에서는 빈 문자열)
content String 미리 렌더링된 목록 HTML(항목 또는 페이지)

목록을 직접 구성하려면 get_taxonomy()를 사용합니다 — 택소노미 참고.


메뉴

site.menus기본 언어의 이름 있는 메뉴(설정의 [[menus.*]] + 프론트 매터의 menus/menu 등록)를 노출합니다. 템플릿 안에서는 site.menus.<name>보다 get_menu(name="...")을 권장합니다 — 현재 페이지의 언어를 기준으로 해석하고, 없으면 기본 언어로 폴백합니다.

{% for item in get_menu(name="main") %}
<a href="{{ item.href }}"{% if item.url | active_path %} aria-current="page"{% endif %}>{{ item.name }}</a>
{% endfor %}

엔트리 속성

속성 타입 설명
name String 표시 레이블
url String 아무것도 붙지 않은 루트 상대 경로, 또는 손대지 않은 외부 URL — page.url과 비교 가능
href String 내부 링크면 url에 사이트 base_path를 적용한 값, 외부면 그대로 — <a href>에는 이 값을 사용
identifier String 메뉴 안에서 고유한 키
weight Int 정렬 순서
external Bool http://, https://, // URL이면 true
children Array\<Entry\> parent가 이 엔트리의 identifier와 일치하는 중첩 엔트리
page Page? 등록한 페이지의 데이터(프론트 매터로 등록된 엔트리만; 설정으로만 등록됐거나 섹션에서 등록된 엔트리는 nil)

전체 설정/프론트 매터 레퍼런스, 계층 구조, 언어별 동작은 메뉴를 참고합니다.


타입 레퍼런스

요약표

타입 설명
String 텍스트 값
String? 선택적 텍스트(nil 가능)
Int 정수
Bool true/false
Array<T> T 타입의 목록
Object 키-값 맵

템플릿에서 확인

{# Check for nil #}
{% if page.description %}...{% endif %}

{# Check for empty array #}
{% if page.authors %}...{% endif %}

{# Check for empty string #}
{% if page.description is present %}...{% endif %}

{# Default value #}
{{ page.description | default(value=site.description) }}

함께 보기