【免费下载链接】pelican
Static site generator that supports Markdown and reST syntax. Powered by Python.
本篇技术指南以仓库中的示例文件 samples/content/draft_article without_date.rst 为切入点,完整剖析 Pelican 静态站点生成器的草稿(draft)机制:如何通过元数据声明一篇草稿、草稿在生成管线中如何被分流与隔离、最终落在哪个输出路径,以及WITH_FUTURE_DATES如何让"未来文章"自动降级为草稿。读完本文,你将掌握草稿文章与草稿页面的配置方法,并理解其背后的源码实现与测试验证方式。
一、原示例逐行解读:一篇最小的草稿文章
关联文档samples/content/draft_article without_date.rst全文仅 7 行,却是理解 Pelican 草稿机制的最佳起点:
A draft article without date ############################ :status: draft This is a draft article, it should live under the /drafts/ folder and not be listed anywhere else.拆解这份 reST 源文件,可以提炼出三个关键技术点:
- 标题:
A draft article without date(reST 的 overline/underline 标题语法,#为最高级标题标记); - 状态元数据:
status: draft—— 这是整篇文章的灵魂,它把内容状态从默认的published切换为draft; - 预期行为描述:注释性正文明确写出了设计意图——草稿应存放于
/drafts/目录下,且不得出现在其他任何列表(索引页、标签页、分类页、Feed 等)。
仓库测试输出目录中恰好存在对应的生成结果 pelican/tests/output/basic/drafts/a-draft-article-without-date.html,实证了该示例的生成行为:草稿被写入drafts/子目录,而不是项目根目录。
二、Pelican 的四种内容状态:published / draft / hidden / skip
草稿机制建立在 Pelican 的内容状态(status)体系之上。在 pelican/contents.py 中,Page与Article两个核心类都明确声明了合法状态集合与默认状态:
class Page(Content): mandatory_properties = ("title",) allowed_statuses = ("published", "hidden", "draft", "skip") default_status = "published" default_template = "page"class Article(Content): mandatory_properties = ("title", "date") allowed_statuses = ("published", "hidden", "draft", "skip") default_status = "published" default_template = "article"四种状态的含义可以概括为:
| 状态 | 行为 | 典型用途 |
|---|---|---|
published | 正常生成并进入索引、分类、标签、Feed 等所有聚合页面 | 正式发布的内容 |
draft | 仅生成到drafts/目录,不参与任何聚合列表 | 未完成、待审阅的文章或页面 |
hidden | 生成页面,但不出现在索引与聚合中(可通过 URL 直接访问) | 需要 URL 但不上首页的内容 |
skip | 完全跳过,不生成任何输出 | 临时停用某篇内容 |
注意一个关键差异:Article的必填属性(mandatory_properties)包含date,而Page不要求日期——这直接决定了后面要讲到的"无日期草稿"处理逻辑只出现在Article类中。
三、草稿的分流与生成:从源文件到 /drafts/ 目录
草稿的整个生命周期由 pelican/generators.py 中的ArticlesGenerator.generate_context驱动。源码中有一段清晰的按状态分流逻辑:
if article.status == "published": all_articles.append(article) elif article.status == "draft": all_drafts.append(article) elif article.status == "hidden": hidden_articles.append(article) elif article.status == "skip": raise AssertionError("Documents with 'skip' status should be skipped")也就是说,读取器(readers)解析出的每个Article对象会根据其status属性被放入三条不同的流水线。草稿随后被处理为self.drafts与self.drafts_translations(多语言草稿):
self.articles, self.translations = _process(all_articles) self.hidden_articles, self.hidden_translations = _process(hidden_articles) self.drafts, self.drafts_translations = _process(all_drafts)最终由generate_drafts把草稿写出:
def generate_drafts(self, write): """Generate drafts pages.""" for draft in chain(self.drafts_translations, self.drafts): write( draft.save_as, self.get_template(draft.template), self.context, article=draft, ... )注意这里的chain(self.drafts_translations, self.drafts):翻译版本(drafts_translations)会先于默认语言的草稿写出。该方法的调用位置在generate_pages内部,排在分类、标签、作者页生成之后——草稿是整条生成管线的最后一环,这也从侧面印证了"草稿不参与聚合"的设计:它们独立走一条通道输出。
四、草稿的输出路径配置:DRAFT_URL 与 DRAFT_SAVE_AS
"草稿应存放于 /drafts/ 文件夹下"并非硬编码,而是由默认配置决定的。在 pelican/settings.py 中可以看到完整的草稿路径配置族:
"DRAFT_URL": "drafts/{slug}.html", "DRAFT_SAVE_AS": "drafts/{slug}.html", "DRAFT_LANG_URL": "drafts/{slug}-{lang}.html", "DRAFT_LANG_SAVE_AS": "drafts/{slug}-{lang}.html", "DRAFT_PAGE_URL": "drafts/pages/{slug}.html", "DRAFT_PAGE_SAVE_AS": "drafts/pages/{slug}.html", "DRAFT_PAGE_LANG_URL": "drafts/pages/{slug}-{lang}.html", "DRAFT_PAGE_LANG_SAVE_AS": "drafts/pages/{slug}-{lang}.html",DRAFT_URL/DRAFT_SAVE_AS:文章类草稿的 URL 与输出文件路径;DRAFT_LANG_*:非默认语言的草稿文章,会带上语言后缀(如my-draft-en.html);DRAFT_PAGE_*:页面(Page)类草稿,默认落在drafts/pages/子目录。
这些占位符({slug}、{lang})会在生成时被替换。此外,pelican/contents.py 中的_expand_settings方法负责按状态选择对应的配置族:
class Article(Content): def _expand_settings(self, key: str) -> str: klass = "draft" if self.status == "draft" else "article" return super()._expand_settings(key, klass)Page类则使用"draft_page"作为草稿状态的键。这意味着:只要你把status改为draft,URL 与输出路径会自动切换到drafts/前缀,无需任何额外配置。同理,若你希望草稿出现在其他位置,直接覆盖上述任一设置项即可(例如DRAFT_SAVE_AS = "wip/{slug}.html")。
五、无日期草稿的特殊处理:datetime.max 技巧
本文示例的标题是 "A draft articlewithout date",而Article的必填属性却包含date。这两者如何兼容?答案在 pelican/contents.py 的Article.__init__中:
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # handle WITH_FUTURE_DATES (designate article to draft based on date) if not self.settings["WITH_FUTURE_DATES"] and hasattr(self, "date"): if self.date.tzinfo is None: now = datetime.datetime.now() else: now = datetime.datetime.now(datetime.UTC) if self.date > now: self.status = "draft" # if we are a draft and there is no date provided, set max datetime if not hasattr(self, "date") and self.status == "draft": self.date = datetime.datetime.max.replace(tzinfo=self.timezone)这里包含两条核心逻辑:
- 未来日期自动降级:当
WITH_FUTURE_DATES为False(默认值为True)时,若文章日期晚于当前时刻,状态会被强制改为draft。这是时间轴类博客"预约发布"的底层实现; - 无日期草稿兜底:当草稿没有声明日期时,Pelican 会为其赋予
datetime.datetime.max(以站点时区self.timezone为准)。这一技巧保证了草稿在按日期排序时永远排在末尾,且不会因为缺少必填属性date而在后续处理中报错。
因此,示例中的无日期草稿可以顺利通过读取与生成流程,最终以最大时间戳参与内部排序,安静地待在所有正常文章之后。
六、草稿的隔离性:不出现在索引、分类、标签与 Feed
"not be listed anywhere else"是草稿机制最核心的隔离语义。从 pelican/generators.py 的源码可以清晰看到它的实现方式:
for article in self.articles: # only main articles are listed in categories and tags # not translations or hidden articles if hasattr(article, "category"): self.categories[article.category].append(article) if hasattr(article, "tags"): for tag in article.tags: self.tags[tag].append(article) for author in getattr(article, "authors", []): self.authors[author].append(article)注意这段循环遍历的是self.articles(已发布文章列表),草稿与隐藏文章根本不会进入分类、标签、作者聚合。同理,索引页、归档页、Feed 的生成也都基于self.articles,草稿因此天然被排除在外。输出目录 pelican/tests/output/basic/ 的目录结构也证实了这一点:drafts/目录中只有两篇草稿(a-draft-article.html与a-draft-article-without-date.html),而index.html、tags.html、categories.html、feeds/中均无草稿条目。
如果你需要预览草稿,只能直接访问其 URL(如http://localhost:8000/drafts/a-draft-article.html),或者临时把状态改回published。
七、草稿的翻译与多语言支持
对于多语言站点,Pelican 对草稿同样提供完整的翻译支持:
- 默认语言的草稿进入
self.drafts; - 其他语言的草稿进入
self.drafts_translations,输出为DRAFT_LANG_SAVE_AS指定的路径(默认drafts/{slug}-{lang}.html); - 生成时翻译版本优先写出(
chain(self.drafts_translations, self.drafts))。
这一设计与已发布文章的articles/translations双列表结构完全对称(pelican/generators.py 中process_translations调用),保证翻译草稿不会与默认语言草稿互相覆盖。
八、草稿页面的支持:Page 类的 draft 状态
草稿机制不仅适用于文章,也适用于页面(Page)。pelican/contents.py 中Page类的_expand_settings使用"draft_page"作为键,对应 pelican/settings.py 中的DRAFT_PAGE_URL与DRAFT_PAGE_SAVE_AS(默认drafts/pages/{slug}.html)。
仓库测试目录 pelican/tests/TestPages/ 中的draft_page.rst、draft_page_markdown.md、draft_page_with_template.rst等文件就是页面草稿的测试样本,而 pelican/tests/output/basic/pages/ 中只出现了已发布的测试页面,草稿页面同样被隔离在drafts/pages/下。
九、测试验证:如何确认草稿机制行为正确
仓库的测试套件为草稿机制提供了直接的行为断言。在 pelican/tests/test_generators.py 中:
def test_articles_draft(self): draft_articles_expected = [ ["Draft article", "draft", "Default", "article"], ] self.assertEqual(sorted(draft_articles_expected), sorted(self.drafts))测试使用distill_articles提取每篇文章的[title, status, category.name, template]四元组,然后断言generator.drafts中只包含状态为draft的样本。与之对称的test_articles_hidden则验证hidden状态被单独收集。此外,pelican/tests/test_cache.py 中也有对generator.drafts的缓存一致性校验(uncached_drafts与cached_drafts排序后相等),确保启用内容缓存时草稿收集结果不变。
十、实践小结:声明一篇草稿的三步走
基于以上分析,在 Pelican 中启用草稿机制只需三步:
- 在文章元数据中声明状态:reST 使用
:status: draft,Markdown 使用Status: draft; - 运行生成命令:
pelican content(或invoke build),草稿会自动输出到drafts/目录; - 预览草稿:直接访问
drafts/下对应 URL;如需临时公开展示,将状态改回published或删除status元数据(默认即为published)。
可选的进阶配置包括:覆盖DRAFT_URL/DRAFT_SAVE_AS改变草稿输出位置;将WITH_FUTURE_DATES设为False以启用"未来文章自动进入草稿"的预约发布行为。草稿机制让你可以在完全不影响线上内容的前提下,把未完成的文章安全地纳入同一套内容目录与构建流程。
最后提醒一点:草稿文件的命名并不影响其状态判定——本文示例文件名中带有空格(draft_article without_date.rst),Pelican 依然能正常处理,因为状态完全由元数据驱动,而非文件名或目录位置决定。
【免费下载链接】pelican
Static site generator that supports Markdown and reST syntax. Powered by Python.
相关推荐
Pelican 草稿(Draft)文章机制详解:用 :status: draft 元数据管理未发布内容
Pelican 草稿(Draft)文章机制详解:用 :status: draft 元数据管理未发布内容 本文以仓库中的示例文件 samples/content/
Pelican 草稿(Draft)机制全解析:从 `draft_page.rst` 看 status 元数据与草稿生成管线
Pelican 草稿(Draft)机制全解析:从 draft_page.rst 看 status 元数据与草稿生成管线 本指南以 Pelican 测试套件中的
Hugo 命令详解:`hugo list drafts`——一键列出全部草稿内容(CSV 输出与过滤机制)
Hugo 命令详解: hugo list drafts ——一键列出全部草稿内容(CSV 输出与过滤机制) 导读 hugo list drafts 是 Hugo
开发工具前端CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考