예제
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" 전체 페이지의 기본 변경 주기
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>
  </url>
  <url>
    <loc>https://example.com/about/</loc>
  </url>
</urlset>

페이지 제외

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

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

RSS 피드

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

설정

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

섹션 피드

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

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

이렇게 하면 /blog/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 속성이 들어갑니다. 피드 제목에는 언어 이름이 포함됩니다 (예: "My Site (한국어)").

커스텀 피드 템플릿

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

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

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

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

컨텍스트 변수

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

변수 타입 설명
feed.type string "rss" 또는 "atom" ([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 날짜; 날짜 없는 페이지는 없음
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 = falsefalse)
authors array 프론트 매터 authors
categories array 택소노미 항목 — tags가 먼저, 그다음 다른 택소노미, 중복 제거
section string 페이지 섹션 경로
language string? 페이지 언어 코드

예시

값은 미리 이스케이프되지 않습니다xml_escape를 적용하거나 CDATA로 감싸는 일은 템플릿 작성자의 몫입니다:

<?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 %}

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는 기본 전체 허용 규칙을 생성합니다. 규칙의 allowdisallow가 모두 비어 있으면 모호한 동작을 막기 위해 명시적인 Allow: /가 추가됩니다.

출력

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
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는 모든 페이지에 Article과 BreadcrumbList JSON-LD를 자동 생성합니다.

<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 + 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 카드 타입
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>

함께 보기