A string of commits on the morning of August 4 had one theme: clearing sensitive values out of the blog config — the Baidu/Bing push tokens, the api_key used for LLM tagging, the passphrase for encrypted post categories — all moved into _config.local.yml of .gitignore, leaving only a _config.local.example.yml template in the repo.

Three related things happened the same day: the comment system moved from Twikoo+Waline to Artalk, the license changed from CC BY-NC-SA 4.0 to CC BY 4.0, and AI crawlers were let through with llms.txt generated. This post is about the cost of config layering, plus the GEO and attribution side of things.

De-sensitizing isn’t deleting the values

_config.yml is public, so writing a key into it is the same as publishing it. But a Hexo build still needs the real values, so the approach is layering:

# _config.local.example.yml(进仓库)
hexo_submit_urls_to_search_engine:
  baidu_token: ""
  bing_token: ""
llm_tagging:
  api_key: ""
encrypt:
  categories:
    - { name: 加密, password: "" }

Copy it to _config.local.yml and fill in the real values; the build deep-merges them with --config _config.yml,_config.local.yml. All four scripts — build / server / dev / push — have the merge arguments built in.

The upside is obvious: no real values in the repo, and the template tells the next person what to fill in. The downside is just as obvious: every command has to remember to carry that second config file, and forgetting it doesn’t throw an error — it degrades silently.

The two directions of forgetting

The first direction is a build that fails outright. After api_key was moved out of llm_tagging, _config.yml is left with only an empty parent key, and running hexo clean without --config throws an error on the empty config. So the hexo clean in clean and dev got the merge arguments too — even a command like “clean”, which looks unrelated to config, has to carry them.

The second direction is subtler: leave out the passphrase for encrypt.categories and the build won’t fail — posts in encrypted categories are simply output in plaintext. So there’s a warning line next to the encrypt block in _config.yml:

# ⚠ 不带 _config.local.yml 合并构建时,加密分类的文章会以明文输出

A config whose failure mode is “leak” is far more dangerous than one whose failure mode is “error”. The former needs someone to actively go look; the latter at least goes red.

The GEO part

Also that day: robots.txt now explicitly allows the major AI crawlers — GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot and friends — and points at llms.txt and the two sitemaps; a new scripts/llms-txt.js was added, generating /llms.txt at build time.

The generator groups by first category, one - [标题](绝对URL): 摘要 line per post, and the summary strips code blocks, HTML and Markdown marks, truncated to 100 characters. Two edge cases are worth mentioning:

  • Encrypted posts are kept out of llms.txt so the summary can’t leak. After hexo-blog-encrypt processing, password is deleted from the post object, so the check has to re-read the frontmatter from the source file — the in-memory object can’t be trusted;
  • An individual post can opt out with llms: false in its front-matter.

On the sitemap side I tried hexo-generator-seo-friendly-sitemap's index-style splitting and rolled back to the official hexo-generator-sitemap two and a half minutes later: the site has only about 80 posts, so splitting into post/page/category/tag tables buys too little, and the official plugin is more actively maintained anyway. The llms.txt and robots changes stayed.

Attribution and license

Also that day, seven AI-drafted posts uniformly got copyright_author: Claude Opus 4.8 and copyright_author_href added, so the “article author” in the copyright box shows the collaborator instead of just me.

The license moved from CC BY-NC-SA 4.0 to CC BY 4.0: attribution, commercial use and derivatives allowed. I thought one thing through while making the change — a fair amount of what’s on the blog was already assembled from client-side corpora, official docs and material from all over, and the friction of the non-commercial restriction outweighed the benefit.

Wrapping up

After config layering, the public repo really is clean, but the system’s default state changed too: previously "just run hexo g" would build a correct site; now you have to remember the merge. Wiring it into every entry point only makes it the default, not automatic — anyone cloning the repo still has to cp the template and fill in values.

If someone asks again someday why the keys can’t just go back into _config.yml, the answer isn’t the word “insecure” — it’s: the public config and the buildable config are two different things from now on.