sqzass.git

sqzass

~ A static site generator written in Rust
git clone https://github.com/debuman/sqzass.git
commit 68d97a07d0c94cbd76f6994e881f6a77c6a0f9aa
parent bf1a9dcb0855c5d03db06ab3bcbb80ed1f63fb52
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Wed, 29 Jul 2026 12:06:10 +0900

본문 있는 단일 페이지 섹션은 빈 섹션이 아니다

empty-section은 만들다 만 디렉터리를 잡는 규칙인데, _index.md가 본문을
가진 단일 페이지 섹션까지 잡았다 — 눌렀을 때 보이는 건 빈 목록이 아니라
그 본문이므로 경고할 결함이 없다. 벤치마크 섹션이 정확히 그 구조라서
드러났다. 페이지도 본문도 없을 때만 경고하고, doctor 검사 표의 서술도
그렇게 고친다.

Diffstat:
Mdocs/content/start/cli.ko.md | 2+-
Mdocs/content/start/cli.md | 2+-
Msrc/doctor.rs | 43++++++++++++++++++++++++++++++++++++++++---
3 files changed, 42 insertion(+), 5 deletion(-)

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 =