예제
KO

SEO

Hwaro는 사이트맵, RSS 피드, robots.txt, 소셜 공유 메타 태그 같은 SEO 기능을 기본으로 제공합니다.

사이트맵

검색 엔진용 sitemap.xml을 자동 생성합니다.

설정

[sitemap]
enabled = true
filename = "sitemap.xml"
changefreq = "weekly"
priority = 0.5
exclude = ["/private", "/drafts"]
키 타입 기본값 설명
enabled bool false sitemap.xml 생성 여부
filename string "sitemap.xml" 출력 파일 이름
changefreq string "weekly" 전체 페이지의 기본 변경 주기: always, hourly, daily, weekly, monthly, yearly, never 중 하나 (다른 값은 경고 후 "weekly" 유지, ""는 태그 생략)
priority float 0.5 전체 페이지의 기본 우선순위(0.0–1.0)
exclude array [] 제외할 경로 접두사 (예: ["/private"]는 /private, /private/page.html을 제외)

출력

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/</loc>
    <lastmod>2024-01-15</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.5</priority>
  </url>
  <url>
    <loc>https://example.com/about/</loc>
    <changefreq>weekly</changefreq>
    <priority>0.5</priority>
  </url>
</urlset>

페이지 제외

프론트 매터에 in_sitemap = false를 설정합니다:

+++
title = "Private Page"
in_sitemap = false
+++

RSS 피드

사이트와 섹션의 RSS 피드를 생성합니다.

설정

[feeds]
enabled = true
type = "rss"              # "rss", "atom" 또는 "json"
limit = 20                # 최대 항목 수
truncate = 0              # N자로 자르기 (0 = 자르지 않음)
full_content = true       # true = 전체 HTML 본문, false = description/요약만
filename = ""             # 비워 두면 기본값 (rss.xml, atom.xml 또는 feed.json)
sections = []             # 특정 섹션으로 제한, 예: ["posts"]
옵션 기본값 설명
enabled false 피드 생성 여부
type "rss" 피드 형식: "rss", "atom" 또는 "json" (JSON Feed 1.1)
limit 10 피드의 최대 항목 수
truncate 0 콘텐츠를 N자로 자르기 (0 = 전체 콘텐츠)
full_content true true = 피드에 전체 HTML 포함, false = 프론트 매터 description 또는 자동 생성 요약 사용
filename "" 메인 피드의 커스텀 파일 이름 (비어 있으면 rss.xml, atom.xml 또는 feed.json). 섹션·언어별·택소노미 용어 피드는 항상 기본 이름을 씁니다
sections [] 피드를 특정 섹션으로 제한
default_language_only true 다국어: 메인 피드에 기본 언어만 포함

JSON Feed

type = "json"으로 설정하면 JSON Feed 1.1 문서를 feed.json으로 생성합니다 (filename을 지정하면 그 이름을 씁니다). RSS·Atom과 같은 위치 모두에 생성됩니다. 메인 피드, 섹션 피드, 언어별 피드, 택소노미 용어 피드가 해당하며, limit, truncate, full_content, sections, default_language_only 설정도 똑같이 적용됩니다.

각 항목에는 id와 url(페이지 퍼머링크), title, summary(RSS <description>과 같은 텍스트), date_published, date_modified(updated에서), tags, authors, image(절대 URL)가 들어갑니다. 본문은 content_html이며, truncate나 full_content = false 때문에 일반 텍스트가 되면 content_text로 들어갑니다. 페이지에 값이 없는 선택 필드는 생략됩니다.

{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "My Site",
  "home_page_url": "https://example.com/",
  "feed_url": "https://example.com/feed.json",
  "items": [
    {
      "id": "https://example.com/posts/hello/",
      "url": "https://example.com/posts/hello/",
      "title": "Hello",
      "content_html": "<p>…</p>",
      "date_published": "2026-03-05T00:00:00Z",
      "tags": ["crystal"]
    }
  ]
}

섹션 피드

섹션별 피드를 활성화합니다:

+++
title = "Blog"
generate_feeds = true
+++

이렇게 하면 /blog/rss.xml이 생성됩니다.

루트 content/_index.md(또는 _index.ko.md 같은 언어 루트)에서는 generate_feeds가 효과가 없습니다. 그 피드는 사이트(또는 언어) 피드와 같은 /rss.xml에 쓰이게 되므로, 더 많은 항목을 담은 사이트 피드를 유지하고 빌드가 경고를 출력합니다.

출력

다국어 피드

사이트가 다국어이면 피드는 언어별로 자동 생성됩니다:

언어 피드 경로 내용
기본 언어 (예: en) /rss.xml 기본 언어 페이지만 (설정 가능)
기본 외 언어 (예: ko) /ko/rss.xml 한국어 페이지만
기본 외 언어 (예: ja) /ja/rss.xml 일본어 페이지만

기본적으로 메인 사이트 피드에는 기본 언어 페이지만 포함됩니다(default_language_only = true). 메인 피드에 모든 언어를 포함하려면 default_language_only = false로 설정합니다. generate_feed = true인 기본 외 언어는 이 설정과 무관하게 각자 별도 피드를 갖습니다.

[feeds]
enabled = true
default_language_only = true   # true (기본값): 메인 피드 = 기본 언어만
                               # false: 메인 피드에 모든 언어 포함

언어별 피드 제어:

[languages.ko]
language_name = "한국어"
generate_feed = true    # /ko/rss.xml 생성 (기본값: true)

[languages.ja]
language_name = "日本語"
generate_feed = false   # /ja/rss.xml을 생성하지 않음

언어 피드는 [feeds] 설정의 sections, limit, truncate, full_content 값을 그대로 공유합니다. RSS 언어 피드에는 <language> 태그가, Atom 피드에는 xml:lang 속성이, JSON 피드에는 최상위 "language" 필드가 들어갑니다. 피드 제목에는 언어 이름이 포함됩니다 (예: "My Site (한국어)").

커스텀 피드 템플릿

피드 마크업을 직접 제어하려면 피드 출력 파일 이름을 딴 템플릿을 만듭니다:

피드 종류 템플릿 파일 로드 키
RSS templates/rss.xml.jinja rss.xml
Atom templates/atom.xml.jinja atom.xml
JSON Feed templates/feed.json.jinja feed.json

템플릿 확장자는 무엇이든 됩니다(.jinja, .j2, .jinja2, .html). 마지막 확장자만 제거되므로 rss.xml.jinja는 rss.xml 키로 로드됩니다. 확장자와 무관하게 파일은 항상 Jinja로 렌더링됩니다(.ecr 파일도 인식되지만 ECR <%= %> 태그는 문자 그대로 출력되므로 Jinja 문법을 써야 합니다). 템플릿 파일의 존재 자체가 옵트인입니다: 파일이 없으면 Hwaro는 기존과 똑같이 내장 피드를 내보내고, 템플릿을 삭제하면 내장 출력으로 되돌아갑니다. 오버라이드는 네 가지 피드 전부(메인 피드, 섹션별 피드, 언어별 피드, 택소노미 항목별 피드)에 적용되고, 커스텀 [feeds] filename은 여전히 메인 피드의 출력 경로를 결정합니다.

피드 템플릿 안에서 {% include %}를 쓸 수 있고, 템플릿이 잘못되면 해당 파일 이름을 담은 템플릿 오류와 함께 빌드가 실패합니다.

컨텍스트 변수

feed — 렌더링 중인 피드의 메타데이터:

변수 타입 설명
feed.type string "rss", "atom" 또는 "json" ([feeds] type을 따름)
feed.kind string "main", "section", "language", "taxonomy" 중 하나
feed.title string 피드 제목 (사이트 제목, Site - Section, Site (한국어) 등)
feed.description string 사이트 설명
feed.url string 이 피드 파일의 절대 self URL (퍼센트 인코딩)
feed.home_url string 피드가 대표하는 표준 HTML URL (사이트 루트, 섹션 페이지, 언어 홈)
feed.base_url string 끝 슬래시를 뺀 base_url
feed.language string? 언어별 피드의 언어 코드, 그 외에는 없음
feed.updated time 가장 최신 항목의 날짜 (결정적; 날짜 있는 항목이 없으면 epoch)
feed.updated_rfc3339 string feed.updated의 RFC 3339 표현 (Atom <updated>)
feed.updated_rfc822 string feed.updated의 RFC 822 표현 (RSS <lastBuildDate>/<pubDate> 형식)
feed.author string 사이트 제목 (없으면 피드 제목으로 대체)
feed.section_url string? 섹션 URL — 섹션 피드 전용
feed.taxonomy / feed.term string? 택소노미 이름과 항목 — 택소노미 피드 전용

pages — 정렬과 limit이 적용된 항목 목록. 각 항목:

변수 타입 설명
title string 페이지 제목 (비어 있으면 사이트 제목)
url string 절대 페이지 URL (퍼센트 인코딩)
date / updated time? 프론트 매터의 원본 날짜 (date 필터에 사용 가능)
date_rfc822 string? 미리 형식화된 RFC 822 날짜; 날짜 없는 페이지는 없음
date_rfc3339 string? date를 RFC 3339 UTC로 형식화한 값 (JSON Feed date_published); 날짜 없는 페이지는 없음
updated_rfc3339 string updated/date 기반 RFC 3339 타임스탬프 (없으면 epoch)
description string? 프론트 매터 description
summary string 일반 텍스트 요약 (description → <!-- more --> 요약 → 발췌 순)
content string full_content/truncate를 반영한 본문 (자를 때는 일반 텍스트)
content_html string 외부 리더를 위해 링크를 절대 URL로 바꾼 전체 HTML 본문
content_is_html bool content가 HTML인지 여부 (truncate/full_content = false면 false)
authors array 프론트 매터 authors
image string? 페이지 image의 절대 URL (퍼센트 인코딩됨)
categories array 택소노미 항목 — tags가 먼저, 그다음 다른 택소노미, 중복 제거
section string 페이지 섹션 경로
language string? 페이지 언어 코드

예시

값은 미리 이스케이프되지 않습니다. xml_escape를 적용하거나 CDATA로 감싸는 일은 템플릿 작성자의 몫입니다. feed.json 템플릿에서는 tojson 필터를 쓰세요:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>{{ feed.title | xml_escape }}</title>
    <link>{{ feed.home_url | xml_escape }}</link>
    <description>{{ feed.description | xml_escape }}</description>
    {% if feed.language %}<language>{{ feed.language | xml_escape }}</language>{% endif %}
    <atom:link href="{{ feed.url | xml_escape }}" rel="self" type="application/rss+xml" />
    {% for p in pages %}
    <item>
      <title>{{ p.title | xml_escape }}</title>
      <link>{{ p.url | xml_escape }}</link>
      <guid>{{ p.url | xml_escape }}</guid>
      <description>{{ p.summary | xml_escape }}</description>
      {% if p.date_rfc822 %}<pubDate>{{ p.date_rfc822 }}</pubDate>{% endif %}
      {% for term in p.categories %}<category>{{ term | xml_escape }}</category>{% endfor %}
    </item>
    {% endfor %}
  </channel>
</rss>

템플릿 링크

<link rel="alternate" type="application/rss+xml" 
      href="{{ base_url }}/rss.xml" 
      title="{{ site.title }}">

{% if page.language and page.language != "en" %}
<link rel="alternate" type="application/rss+xml"
      href="{{ base_url }}/{{ page.language }}/rss.xml"
      title="{{ site.title }} ({{ page.language }})">
{% endif %}

JSON Feed는 type="application/feed+json"과 feed.json을 씁니다:

<link rel="alternate" type="application/feed+json"
      href="{{ base_url }}/feed.json"
      title="{{ site.title }}">

Robots.txt

검색 엔진 크롤링을 제어합니다.

설정

[robots]
enabled = true

커스텀 규칙 사용:

[robots]
enabled = true
rules = [
  { user_agent = "*", disallow = ["/admin", "/private"] },
  { user_agent = "GPTBot", disallow = ["/"] }
]
키 타입 기본값 설명
enabled bool true robots.txt 생성 여부
filename string "robots.txt" 출력 파일 이름
rules array [] allow/disallow 경로를 담은 user-agent 규칙 목록

규칙을 설정하지 않으면 Hwaro는 기본 전체 허용 규칙을 생성합니다. 규칙의 allow와 disallow가 모두 비어 있으면 모호한 동작을 막기 위해 명시적인 Allow: /가 추가됩니다.

Content Signals

규칙에 content_signal 테이블을 넣으면 Content Signals 줄을 출력합니다. 크롤러가 가져간 콘텐츠를 어떻게 써도 되는지 알리는 지시어입니다:

[robots]
rules = [
  { user_agent = "*", allow = ["/"], content_signal = { search = true, ai_input = true, ai_train = false } },
]
User-agent: *
Content-Signal: search=yes, ai-input=yes, ai-train=no
Allow: /
키 시그널 의미
search search 검색 색인을 만들고 링크와 짧은 발췌문을 보여 주는 용도
ai_input ai-input AI 모델의 입력으로 콘텐츠를 넣는 용도 (검색 증강, 그라운딩)
ai_train ai-train AI 모델 학습이나 파인튜닝

값은 true(yes) 또는 false(no)입니다. 생략한 키는 출력하지 않으며, 시그널은 항상 위 표의 순서로 나열됩니다. 알 수 없는 키나 불리언이 아닌 값은 설정 오류입니다. content_signal이 없는 규칙의 출력은 이전과 똑같습니다.

출력

User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml

LLMs.txt

llms.txt 표준을 따르는 AI/LLM 크롤러용 안내 파일을 생성합니다.

[llms]
enabled = true
instructions = "This site's content is provided under the MIT license."
full_enabled = true

전체 설정과 출력 형식은 LLMs.txt에서 다룹니다.


OpenGraph 태그

Facebook, LinkedIn 등을 위한 소셜 공유 메타 태그입니다.

설정

[og]
default_image = "/images/og-default.png"
type = "website"
fb_app_id = "your_fb_app_id"
키 설명
default_image 페이지에 이미지가 없을 때 사용할 대체 이미지
type 콘텐츠 페이지의 OpenGraph 타입 (기본값: "article"; 목록 페이지는 항상 "website")
fb_app_id Facebook 앱 ID (선택)

페이지 단위 오버라이드

+++
title = "My Article"
description = "Article description"
image = "/images/article-cover.png"
+++

템플릿에서 사용

<head>
  {{ og_tags | safe }}
</head>

출력

<meta property="og:title" content="My Article">
<meta property="og:type" content="article">
<meta property="og:url" content="https://example.com/my-article/">
<meta property="og:description" content="Article description">
<meta property="og:image" content="https://example.com/images/article-cover.png">

Twitter 카드

Twitter 전용 공유 태그입니다.

설정

[og]
twitter_card = "summary_large_image"
twitter_site = "@yourusername"
twitter_creator = "@authorusername"
키 설명
twitter_card 카드 타입: summary, summary_large_image (이미지가 없는 페이지는 summary. 이미지 없는 큰 카드는 빈 미리보기로 표시됩니다)
twitter_site 사이트의 Twitter 핸들
twitter_creator 작성자의 Twitter 핸들

템플릿에서 사용

<head>
  {{ twitter_tags | safe }}
</head>

또는 OG와 Twitter 태그를 함께 포함합니다:

<head>
  {{ og_all_tags | safe }}
</head>

출력

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="My Article">
<meta name="twitter:description" content="Article description">
<meta name="twitter:image" content="https://example.com/images/article-cover.png">
<meta name="twitter:site" content="@yourusername">

JSON-LD 구조화된 데이터

Hwaro는 모든 페이지에 페이지 단위 JSON-LD를 자동 생성합니다. 콘텐츠 페이지에는 Article(목록 페이지는 CollectionPage, 홈 페이지는 WebSite)과 BreadcrumbList가 생성됩니다.

<head>
  {{ jsonld | safe }}
</head>

FAQ, HowTo, WebSite, Organization 같은 추가 스키마 타입도 사용할 수 있습니다. 전체 타입, 설정, 출력 예시는 구조화된 데이터에서 다룹니다.


템플릿 변수

미리 렌더링된 HTML

다음 변수는 바로 쓸 수 있는 HTML 태그를 출력합니다:

변수 설명
og_tags OpenGraph 메타 태그
twitter_tags Twitter 카드 메타 태그
og_all_tags OG와 Twitter 태그 전체
canonical_tag canonical 링크 태그
hreflang_tags hreflang 대체 링크 태그
jsonld Article(또는 CollectionPage/WebSite) + BreadcrumbList JSON-LD
jsonld_article Article JSON-LD만
jsonld_breadcrumb BreadcrumbList JSON-LD만
page_description 페이지 설명 (없으면 사이트 설명)
page_image 페이지 이미지 (없으면 og.default_image)

SEO 객체

seo 객체는 커스텀 메타 태그를 만들 수 있도록 필드 단위 접근을 제공합니다:

속성 타입 설명
seo.canonical_url String 전체 canonical URL
seo.og_type String OpenGraph 타입 (기본값: "article")
seo.og_image String 해석된 절대 이미지 URL
seo.twitter_card String 이 페이지에 twitter_tags가 출력하는 Twitter 카드 타입
seo.twitter_site String Twitter 사이트 핸들
seo.twitter_creator String Twitter 작성자 핸들
seo.fb_app_id String Facebook 앱 ID
seo.hreflang Array 언어 번역 링크
<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 %}
  {% if seo.fb_app_id %}
  <meta property="fb:app_id" content="{{ seo.fb_app_id }}">
  {% endif %}
  <meta name="twitter:card" content="{{ seo.twitter_card }}">
  <meta name="twitter:title" content="{{ page.title }}">
  {% if seo.twitter_site %}
  <meta name="twitter:site" content="{{ seo.twitter_site }}">
  {% endif %}
</head>

전체 예시

config.toml

title = "My Site"
description = "A great site"
base_url = "https://example.com"

[sitemap]
enabled = true

[feeds]
enabled = true
limit = 20

[robots]
enabled = true

[og]
default_image = "/images/og-default.png"
type = "website"
twitter_card = "summary_large_image"
twitter_site = "@mysite"

templates/base.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>{{ page.title }} - {{ site.title }}</title>
  <meta name="description" content="{{ page.description | default(value=site.description) }}">
  {{ og_all_tags | safe }}
  {{ canonical_tag | safe }}
  {{ hreflang_tags | safe }}
  {{ jsonld | safe }}
  <link rel="alternate" type="application/rss+xml" href="{{ base_url }}/rss.xml">
</head>
<body>
  {% block content %}{% endblock %}
</body>
</html>

함께 보기