diff --git a/docs/content/start/cli.ko.md b/docs/content/start/cli.ko.md
@@ -63,7 +63,7 @@ URL을 주장하는 두 페이지, 오타 난 설정 키. `doctor`는 **빌드
|---|---|---|
| `base-url` | warn | `base_url`이 아직 자리표시자 `https://example.com`입니다. |
| `untranslated` | warn | 어떤 언어에는 있고 어떤 언어에는 없는 페이지입니다. |
-| `empty-section` | warn | 섹션에 페이지가 없어서 내비게이션 항목이 빈 곳으로 갑니다. |
+| `empty-section` | warn | 섹션에 페이지도 `_index.md` 본문도 없어서 내비게이션 항목이 빈 곳으로 갑니다. 본문이 있는 단일 페이지 섹션은 괜찮습니다. |
| `description` | note | `description`이 없는 페이지입니다. |
| `draft` | note | 빌드에서 빠지는 페이지입니다. |
| `unused-template` | note | 어떤 페이지도 고르지 않고, 어떤 템플릿도 이름으로 부르지 않습니다. |
diff --git a/docs/content/start/cli.md b/docs/content/start/cli.md
@@ -63,7 +63,7 @@ missing template, two pages claiming one URL, a misspelled configuration key.
|---|---|---|
| `base-url` | warn | `base_url` is still the placeholder `https://example.com`. |
| `untranslated` | warn | A page exists in some languages and not others. |
-| `empty-section` | warn | A section has no pages, so its navigation entry leads nowhere. |
+| `empty-section` | warn | A section has no pages and its index has no body — the navigation entry leads nowhere. A single-page section whose `_index.md` carries content is fine. |
| `description` | note | A page has no `description`. |
| `draft` | note | A page is excluded from the build. |
| `unused-template` | note | No page selects this template, and no template names it. |
diff --git a/src/doctor.rs b/src/doctor.rs
@@ -172,16 +172,24 @@ fn drafts(root: &Path, cfg: &Config, out: &mut Vec<Finding>) -> Result<()> {
Ok(())
}
-/// 자식이 하나도 없는 섹션. 내비게이션에 들어가지만 눌러도 목록이 비어 있다.
+/// 자식도 본문도 없는 섹션. 내비게이션에 들어가지만 눌러도 아무것도 없다.
+///
+/// 자식이 없어도 `_index.md`에 본문이 있으면 경고하지 않는다 — 눌렀을 때
+/// 보이는 건 빈 목록이 아니라 그 본문이고, 단일 페이지 섹션은 정당한 구조다.
+/// 이 규칙이 잡으려는 건 만들다 만 디렉터리다.
fn empty_sections(cfg: &Config, site: &Site, out: &mut Vec<Finding>) {
for lang in cfg.languages.keys() {
let mut stack: Vec<&crate::site::Section> = site.sections(lang).iter().collect();
while let Some(section) = stack.pop() {
- if section.pages.is_empty() && section.subsections.is_empty() {
+ let index_has_body = section
+ .index
+ .map(|i| !site.pages[i].body.trim().is_empty())
+ .unwrap_or(false);
+ if section.pages.is_empty() && section.subsections.is_empty() && !index_has_body {
out.push(Finding {
severity: Severity::Warn,
check: "empty-section",
- message: format!("'{}' 섹션에 페이지가 없습니다", section.title),
+ message: format!("'{}' 섹션에 페이지도 본문도 없습니다", section.title),
file: Some(section.url.clone()),
});
}
@@ -266,6 +274,35 @@ mod tests {
assert!("loud".parse::<Severity>().is_err());
}
+ #[test]
+ fn a_section_with_a_contentful_index_is_not_empty() {
+ // 단일 페이지 섹션(자식 없음, _index에 본문 있음)은 정당한 구조다.
+ // 본문까지 없는 섹션만 만들다 만 디렉터리로 잡는다.
+ let dir = std::env::temp_dir().join(format!("sqzass-doctor-empty-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&dir);
+ for (d, body) in [("full", "충실한 본문이 있다."), ("bare", "")] {
+ let p = dir.join("content").join(d);
+ std::fs::create_dir_all(&p).unwrap();
+ std::fs::write(
+ p.join("_index.md"),
+ format!("+++\ntitle = \"{d}\"\n+++\n{body}"),
+ )
+ .unwrap();
+ }
+ let cfg: Config = toml::from_str(
+ "title = \"t\"\nbase_url = \"https://x.dev\"\n[languages.en]\nname = \"English\"\n",
+ )
+ .unwrap();
+ let pages = crate::content::discover(&dir, &cfg, false).unwrap();
+ let site = Site::build(&pages, &cfg);
+ let mut out = Vec::new();
+ empty_sections(&cfg, &site, &mut out);
+ let flagged: Vec<_> = out.iter().filter(|f| f.check == "empty-section").collect();
+ assert_eq!(flagged.len(), 1, "{out:?}");
+ assert_eq!(flagged[0].file.as_deref(), Some("/bare/"));
+ let _ = std::fs::remove_dir_all(&dir);
+ }
+
#[test]
fn placeholder_base_url_is_reported() {
let cfg: Config =
|