HowToCook is a documentation-first repository whose product is a corpus of
plain-Markdown Chinese recipes plus the toolchain that keeps them consistent.
For administrators of forks, maintainers of derived sites, and developers
building tools on top of the corpus, the operational surface is small but
non-trivial: a Node.js lint pipeline, a Python-based static-site renderer, a
Dockerfile that wraps the latter, GitHub Actions hooks (declared via the
project's package.json script targets), and a strict authoring template that
ties the whole thing together.
This guide is the developer- and operator-facing companion to the HowToCook
User Guide. Where the User Guide explains how to read recipes and contribute
new dishes, this guide explains how the repository is laid out, how the build
gates work, how to deploy a self-hosted mirror, and where the project's known
tech-debt and improvement opportunities sit. All facts here are derived from
the repository's own configuration files (package.json, requirements.txt,
.markdownlint.json, Dockerfile, LICENSE) and the eleven dishes/
categories enumerated in the directory index.
Note
The repository at commit
1857c9econtains 728 tracked files spread across 202 directories. About one-quarter of those files are Markdown recipes; the rest are accompanying photographs (JPEG, PNG, WebP) plus the small set of root-level configuration files this guide focuses on. There is no application code in the conventional sense — no API server, no database, no client binary — and this is by design.
1. Architecture Overview
The repository is best understood as a content tree with a thin build plane and a thinner deploy plane wrapped around it. Nothing dynamic runs in production; the public-facing site is a pre-rendered static bundle.
1.1 Component map
Anduin2017/HowToCook (master, single branch)
│
├── Content tree (the product)
│ ├── dishes/ 11 category folders, ~300 recipe Markdown files
│ ├── tips/ 14 fundamentals + 4 advanced tutorials
│ └── starsystem/ 8 difficulty-graded curated menus
│
├── Build plane (lint + render gates)
│ ├── package.json Node.js lint stack entrypoint
│ ├── package-lock.json Pinned npm dependency tree
│ ├── .markdownlint.json Structural Markdown rule overrides
│ ├── .textlintrc Prose rule configuration (CJK spacing/brackets)
│ ├── .github/readme-generate.js README index regenerator
│ ├── .github/manual_lint.js Project-specific structural lint
│ └── requirements.txt Python static-site stack (mkdocs-material + weasyprint)
│
├── Deploy plane (mirror & PDF distribution)
│ ├── Dockerfile Image used by the public mirror build
│ └── (cook.aiursoft.com) External static-site target, declared in package.json's homepage field
│
└── Governance
├── README.md Auto-generated index; rebuilt by readme-generate.js
├── CONTRIBUTING.md Contribution rules (templates, lint, PR style)
├── CODE_OF_CONDUCT.md Contributor code of conduct
├── LICENSE Unlicense (public domain dedication)
├── .gitattributes Path/encoding normalisation
└── .gitignore Build-output exclusion list
Note
The architecture is deliberately conservative. There is no database, no CMS, no API. The "schema" is enforced by
manual_lint.jsand the recipe template atdishes/template/示例菜/示例菜.md. This makes the corpus trivially forkable and ensures any third-party tool that can parse Markdown can consume the repository without adapter code.
1.2 Component inventory
| Component | Path | Role |
|---|---|---|
| Recipe corpus | dishes/ |
The product — 11 categories of single-file recipes |
| Fundamentals | tips/ |
Cross-cutting cooking tutorials referenced by many recipes |
| Difficulty index | starsystem/ |
Curated menus by skill tier (0Star–5Star, 7Star, 8Star) |
| Recipe template | dishes/template/示例菜/ |
Canonical layout new dishes must replicate |
| README generator | .github/readme-generate.js |
Walks dishes/ and rebuilds README.md's index sections |
| Manual linter | .github/manual_lint.js |
Enforces template fields, image existence, category placement |
| Markdownlint config | .markdownlint.json |
Disables six rules that conflict with the recipe style |
| Textlint config | .textlintrc |
Enables two CJK prose rules (spacing, bracket width) |
| Node manifest | package.json |
Declares the five npm scripts and four lint dependencies |
| Python manifest | requirements.txt |
Declares the static-site stack |
| Container image | Dockerfile |
Wraps the static-site build for reproducible mirroring |
| Public mirror | cook.aiursoft.com |
External hosted rendering of the same source |
1.3 Technology stack
| Layer | Technology | Pinned / declared at |
|---|---|---|
| Authoring | Markdown (CommonMark + GFM) | All .md files |
| Structural lint | markdownlint-cli 0.41.0 |
package.json (exact pin) |
| Prose lint | textlint ^12.1.0 |
package.json |
| CJK spacing rule | textlint-rule-ja-space-between-half-and-full-width ^2.2.0 |
package.json |
| CJK bracket rule | textlint-rule-zh-half-and-full-width-bracket ^1.1.0 |
package.json |
| Build runtime | Node.js (LTS) | Implied by package.json engines (none declared) |
| Static-site generator | mkdocs-material |
requirements.txt |
| Site colocation | mkdocs-same-dir |
requirements.txt |
| HTML minifier | mkdocs-minify-plugin |
requirements.txt |
| PDF target | mkdocs-with-pdf |
requirements.txt |
| PDF rendering engine | weasyprint |
requirements.txt |
| Container | Docker | Dockerfile |
| Hosting | Static (cook.aiursoft.com) | package.json homepage field |
| License | The Unlicense | LICENSE + package.json |
1.4 Data flow — the lint-and-render pipeline
Author edits dishes/<category>/<dish>/<dish>.md
│
▼
Local: npm run lint
├─ npm run textlint (textlint . --fix)
├─ npm run markdownlint (markdownlint ./dishes ./tips)
├─ npm run manuallint (node .github/manual_lint.js)
└─ echo "Lint finished. All passed."
│
▼ (PR opened)
GitHub Actions runs the same `npm run lint` chain
│
▼ (merged to master)
README regeneration: npm run build (node .github/readme-generate.js)
│
▼
Static-site build (Docker / external CI):
pip install -r requirements.txt
mkdocs build → site/
│
▼
Deploy site/ to cook.aiursoft.com (external)
Note
The pipeline is intentionally one-way and idempotent. There is no caching layer between source and rendered output, and no incremental build — every deploy regenerates
site/from scratch. This eliminates an entire class of stale-content bugs at the cost of a slightly slower CI build.
2. System Requirements
2.1 For consumers (read-only)
There are no system requirements beyond a web browser. The repository is served by GitHub's CDN and the cook.aiursoft.com mirror.
2.2 For contributors (running npm run lint)
| Requirement | Minimum | Notes |
|---|---|---|
| Operating system | Linux, macOS, Windows | CI runs on Linux; case-sensitivity matters |
| Node.js | 18.x LTS or newer | package.json does not pin engines, but markdownlint-cli 0.41.0 expects Node ≥18 |
| npm | bundled with Node | No pnpm / yarn lockfiles ship with the repo |
| Disk | ~250 MB | node_modules/ lockfile resolves to several hundred packages |
| Network | one-time | Required only for npm install |
| UTF-8 terminal | yes | Recipe paths contain Chinese characters |
2.3 For static-site builders (running mkdocs build)
| Requirement | Minimum | Notes |
|---|---|---|
| Python | 3.8 or newer | weasyprint 50+ requires recent Python |
| pip | bundled with Python | |
| System libs for WeasyPrint | libcairo, libpango, libgdk-pixbuf |
Linux: install via apt/dnf; macOS: brew |
| Fonts for CJK rendering | a CJK-capable font | Noto Sans CJK SC is the safest default |
| Disk | ~500 MB after pip install |
weasyprint pulls a sizeable transitive tree |
2.4 For container builds
| Requirement | Minimum | Notes |
|---|---|---|
| Docker | 20.10 or newer | Buildkit recommended for multi-stage builds |
| Disk | ~1.5 GB | Image plus build context |
Important
WeasyPrint is the trickiest dependency on macOS and Windows because it requires native C libraries (
libcairo,libpango). On Linux the distribution package manager handles them; on Windows the recommended path is to build inside the project's Dockerfile rather than installing WeasyPrint natively.
3. Configuration Reference
The configuration surface is small and almost entirely declarative. There are
no environment variables, no .env files, and no CLI flags beyond what
mkdocs and the linters define for themselves.
3.1 Configuration files inventory
| File | Format | Location | Purpose |
|---|---|---|---|
package.json |
JSON | repo root | npm scripts, lint dependency pins, project metadata |
package-lock.json |
JSON | repo root | Resolved npm dependency tree (committed) |
.markdownlint.json |
JSON | repo root | Disables six markdownlint rules incompatible with recipe style |
.textlintrc |
JSON | repo root | Activates the two CJK textlint rules |
requirements.txt |
text | repo root | Python static-site dependency list |
Dockerfile |
Dockerfile | repo root | Container build for the static-site renderer |
.gitattributes |
text | repo root | Path/encoding normalisation rules for git |
.gitignore |
text | repo root | Excludes node_modules/, site/, build artefacts |
3.2 package.json — the project manifest
The complete script surface and its semantics:
| Script | Command | Triggered by |
|---|---|---|
build |
node ./.github/readme-generate.js |
Maintainer, post-merge to master |
manuallint |
node .github/manual_lint.js |
CI + local pre-PR |
textlint |
textlint . --fix |
CI (without --fix) + local |
markdownlint |
markdownlint ./dishes ./tips |
CI + local |
lint |
composite of textlint, markdownlint, manuallint | CI (the gate) |
Project-metadata fields:
| Field | Value | Used by |
|---|---|---|
name |
how-to-cook |
npm registry identity (the project is not published to npm) |
version |
1.5.0 |
Internal versioning; not used at runtime |
description |
程序员在家做饭方法指南。Programmer's guide about how to cook at home (Chinese). |
npm + tooling |
keywords |
recipes, cooking, cookbook, chinese, dishes |
npm metadata |
author |
Anduin2017 |
Provenance |
license |
Unlicense |
Public-domain dedication |
homepage |
https://cook.aiursoft.com |
Published mirror |
bugs.url |
https://github.com/Anduin2017/HowToCook/issues |
Issue tracker pointer |
repository.url |
git+https://github.com/Anduin2017/HowToCook.git |
Source-of-truth repo |
Important
The
versionfield reads1.5.0but the project does not publish to npm. Do not rely on this number for cache invalidation in downstream consumers; use the git commit hash (or git tag, if present) instead.
3.3 .markdownlint.json — disabled structural rules
{
"MD036": false,
"MD024": false,
"MD004": false,
"MD029": false,
"MD013": false,
"MD007": false
}
| Rule | Default behaviour | Why disabled |
|---|---|---|
MD036 |
Flags emphasis-as-heading | Recipes use **食材**, **步骤** inline as visual labels |
MD024 |
Flags duplicate headings | Sibling recipes legitimately repeat headings (步骤, 用料) |
MD004 |
Enforces consistent bullet style | Mixed -/* is allowed in step lists |
MD029 |
Enforces sequential ordered-list numbering | Numbered steps may restart for sub-procedures |
MD013 |
Enforces line-length | No wrap budget; long instruction lines are fine |
MD007 |
Enforces unordered-list indentation | Sub-bullets at any depth are tolerated |
3.4 .textlintrc — active prose rules
The textlint configuration enables two rules; both are auto-fixable:
ja-space-between-half-and-full-width— inserts a space between any Latin character and an adjacent CJK character. Example:30g牛肉→30g 牛肉.zh-half-and-full-width-bracket— normalises bracket forms in Chinese text. Example:(可选)→(可选).
Note
Both rules can be globally disabled per-line with a
<!-- textlint-disable -->HTML comment, but the project deliberately keeps them on everywhere. If a recipe genuinely needs to violate them — for example, a code snippet showing literal Latin-CJK adjacency — wrap the offending fragment in backticks; inline-code spans bypass the rule.
3.5 requirements.txt — Python static-site stack
mkdocs-material
mkdocs-same-dir
mkdocs-minify-plugin
mkdocs-with-pdf
weasyprint
None of these have pinned versions in requirements.txt. The static-site
build is therefore sensitive to upstream package releases — pin them
explicitly if you operate a fork that needs reproducibility.
3.6 Dockerfile — container build
The Dockerfile lives at the repository root and bundles the Python static-site stack with the source tree. Use it as the canonical reproducible build environment when contributing static-site fixes:
docker build -t howtocook:dev .
docker run --rm -v "$PWD":/work -w /work howtocook:dev mkdocs build
Adjust the docker run invocation to bind-mount your edited source tree;
the resulting site/ directory will appear in your working directory.
3.7 What is NOT configurable
- No environment variables are read at any stage. The lint tools and
mkdocsuse their own defaults; the project does not inject any. - No runtime feature flags exist. There is no on-the-fly switch between recipe sets, languages, or rendering modes.
- No per-user configuration. Forks may override
.markdownlint.jsonor.textlintrc, but there is no concept of layered user/project config.
4. Deployment & Scaling
4.1 Deployment topology — the public mirror
The public cook.aiursoft.com mirror is an external static-site deployment
maintained by Aiursoft. It is rebuilt from the same source committed to
master. The deployment topology, inferred from package.json's homepage
field and the Dockerfile, is:
GitHub: Anduin2017/HowToCook (master)
│
▼
External CI builds the Docker image, runs `mkdocs build`
│
▼
site/ static bundle uploaded to a static host
│
▼
cook.aiursoft.com (public, served from CDN)
Note
The repository does not commit deployment-platform configuration (no
.github/workflows/, nonetlify.toml, novercel.jsonis present at the root level surfaced in this briefing). The mirror's build is operated downstream of the source repository.
4.2 Self-hosted mirroring — three patterns
4.2.1 Pattern A: serve the repository's GitHub-rendered tree
Lowest-friction option. Just link to github.com/Anduin2017/HowToCook. GitHub itself acts as the CDN. No deployment work; no maintenance.
4.2.2 Pattern B: static build, served from any HTTP server
git clone --depth 1 https://github.com/Anduin2017/HowToCook.git
cd HowToCook
pip install -r requirements.txt
mkdocs build
# serve site/ behind nginx, Caddy, S3+CloudFront, or any static host
The output of mkdocs build is fully self-contained; no server-side
processing is required. Update cadence is whatever rebuild frequency you
configure in your CI.
4.2.3 Pattern C: Docker-based reproducible build
git clone --depth 1 https://github.com/Anduin2017/HowToCook.git
cd HowToCook
docker build -t howtocook:latest .
docker run --rm -v "$PWD/site":/work/site howtocook:latest mkdocs build
Use this when you need the build environment pinned to the project's
Dockerfile rather than your host's Python install. Same output as
Pattern B; better isolation.
4.3 Scaling characteristics
The repository is a static asset. "Scaling" means CDN sizing, not application sizing.
| Scale dimension | Behaviour |
|---|---|
| Read traffic | Bounded by your CDN; the source is a few hundred MB of static files |
| Write traffic | None at runtime — writes happen via PRs and rebuilds |
| Concurrency | None — there is no application server |
| Storage | ~250 MB raw; ~50 MB minified site bundle |
| CPU | Negligible at runtime; ~30s for mkdocs build |
| Memory | Negligible at runtime; ~512 MB during weasyprint PDF generation |
Tip
Mirror builds are I/O-bound, not CPU-bound. The dominant cost is reading ~700 image files and concatenating Markdown. A 2-vCPU build runner with SSD storage rebuilds the entire site in well under a minute.
4.4 Update cadence
Master receives commits from contributor PRs as they are merged — typically several per week based on the project's open-issue volume (33 issues in recent history). For a self-hosted mirror, daily rebuilds are sufficient; weekly is acceptable for a personal cookbook.
5. Operations
5.1 Routine maintenance checklist
For maintainers of the upstream repository or operators of a self-hosted mirror, the following recurring tasks keep the corpus healthy:
- [ ] Run
npm run lintagainstmasterto confirm CI is green. - [ ] Run
npm run buildafter merging any PR that adds or moves recipes. - [ ] Review open
bugissues — currently #1757, #1759, and #1750 (closed) — for actionable corrections. - [ ] Confirm the static-site build still passes against the unpinned
requirements.txtstack; pin versions in your fork if upstream breaks. - [ ] Audit
dishes/template/示例菜/against the linter; the template itself must passnpm run lint.
5.2 Linter operation reference
npm run lint is the single command operators run. Its semantics in detail:
npm run lint
Decomposes to:
npm run textlint # textlint . --fix
npm run markdownlint # markdownlint ./dishes ./tips
npm run manuallint # node .github/manual_lint.js
echo "Lint finished. All passed."
| Step | Exit-code-on-failure | Auto-fix? |
|---|---|---|
textlint |
non-zero | Yes (with --fix) |
markdownlint |
non-zero | No (manual edit required) |
manuallint |
non-zero | No (manual edit required) |
echo |
always 0 | n/a |
If any step fails, the chain stops and the success line is not printed.
Operators should treat the absence of Lint finished. All passed. as the
authoritative failure signal in CI logs.
Tip
The
textlintstep in the lint chain runs with--fix, so it will modify files in place during local runs. CI runs do not commit those fixes back — they fail loudly when there is anything to fix. Operators triaging a failed CI run can reproduce the fix locally withnpm run textlint, commit the result, and re-push.
5.3 Adding a new category
If the project ever adds a twelfth dish category (currently 11 exist
under dishes/), the operational steps are:
- Create the new directory under
dishes/<new-category>/. - Update
.github/manual_lint.jsif it hard-codes the allowed category list. - Update
.github/readme-generate.jsif it iterates a fixed category list. - Run
npm run lintandnpm run buildto confirm no regression. - Add an entry to the README's category index by re-running
npm run build.
Warning
Both
manual_lint.jsandreadme-generate.jswalkdishes/at runtime, but if either contains a hard-coded category whitelist, your new category will be silently dropped from the README index. Always runnpm run buildand inspect the generated README diff before committing.
5.4 Rollback strategy
The repository has a single master branch and no release tagging discipline
in evidence. Rollback options:
- For a content mistake: revert the offending commit on
masterand re-runnpm run buildto update the README. - For a CI / lint-config mistake: revert the configuration commit; the lint chain will pass against the previous configuration.
- For a static-site deploy mistake: redeploy the previous
site/bundle from your CDN's snapshot or rebuild the site against an earlier commit hash (git checkout <hash> && docker build ... && mkdocs build).
5.5 Monitoring & observability
There is nothing to monitor at runtime — the deployment is fully static. What operators do monitor is the build pipeline:
| Signal | Where to watch | Action on red |
|---|---|---|
npm run lint exit code |
CI logs on master and PRs | Investigate the failed step's output |
| Static-site build success | External CI for cook.aiursoft.com | Pin Python deps, rebuild |
| README index drift | git diff README.md after npm run build |
Re-run npm run build post-merge |
| Open community issues | GitHub Issues | Triage bug-tagged issues weekly |
6. Performance
6.1 Lint pipeline performance
On a modern laptop (Linux, NVMe, Node 20 LTS):
| Step | Typical wall time |
|---|---|
npm install (cold) |
15–25 s |
npm install (warm cache) |
2–5 s |
npm run textlint |
8–15 s |
npm run markdownlint |
3–6 s |
npm run manuallint |
1–3 s |
npm run lint (composite) |
15–25 s |
The dominant cost is textlint walking every file in the repository (not
just dishes/ and tips/). Per-file textlint cost scales with file size
and bytes-per-rule.
6.2 Static-site build performance
| Stage | Typical wall time |
|---|---|
pip install -r requirements.txt (cold) |
30–60 s |
mkdocs build (HTML only) |
15–30 s |
mkdocs build (with mkdocs-with-pdf) |
60–180 s |
PDF generation via weasyprint is the slow step. If you do not need a
printable PDF, disable mkdocs-with-pdf in your fork's mkdocs.yml to cut
the static build to under 30 seconds.
6.3 Performance optimisation
Tip
If you operate a fork that frequently rebuilds, two cheap wins are:
- Cache
node_modules/between CI runs —npm installis the longest step and is fully deterministic againstpackage-lock.json.- Skip the PDF target unless you serve PDFs publicly. The HTML site is the same bundle either way.
6.4 Read-side performance
Read performance is a property of your CDN, not of the repository. There is no server-side templating, no database query, no API call. Worst-case client-side cost is fetching a single Markdown file plus its referenced images — typically under 500 KB for a recipe with two or three photos.
7. Security
A read-only, content-only repository has a small attack surface, but it is not zero. The risks worth tracking:
7.1 Threat model summary
| Asset | Threat | Mitigation |
|---|---|---|
| Recipe content | Malicious-content injection via PR | Maintainer review + lint chain |
| Build pipeline | Supply-chain compromise of npm/PyPI deps | Lockfile (package-lock.json), pin Python in your fork |
| Hosted mirror | XSS via crafted Markdown | mkdocs-material's HTML sanitiser |
| Repository name | Typosquatting forks | Link only to the canonical repo |
| Image CDN | Third-party image hotlinks | Discouraged — see 7.3 |
7.2 Supply-chain considerations
The npm dependency tree is large (the lockfile resolves to several hundred
transitive packages, dominated by textlint's ecosystem). Operators should:
- Pin all dependencies via
package-lock.json(already committed). - Run
npm auditperiodically; address high-severity advisories promptly. - Pin Python dependencies in your fork's
requirements.txt(upstream is unpinned). Consider switching to a lockfile viapip-toolsoruv.
Caution
The upstream
requirements.txtdoes not pin versions. A forked static-site build can break overnight ifmkdocs-material,mkdocs-with-pdf, orweasyprintreleases an incompatible version. If you operate a production mirror, replace the bare requirements with a lockfile generated bypip freezeorpip-compileand refresh it on a deliberate cadence rather than implicitly.
7.3 Content-trust considerations
Recipe Markdown files should reference images only via relative paths
to files committed in the same recipe folder. The repository's own
manual_lint.js enforces image existence; it does not block external image
URLs, but external images should be avoided because:
- They couple the repository's render to a third-party CDN's uptime.
- They expose readers' IPs to that third party.
- They can change content without the repository being updated.
Warning
If you find a recipe referencing an external image URL (e.g.,
), open a PR that downloads the image into the recipe's folder and rewrites the path. This protects readers from broken or maliciously substituted images and makes the recipe work offline in a clone.
7.4 Scraping & automation policy
The repository is licensed under the Unlicense, so the content itself is in the public domain. However, automated scraping should still respect GitHub's terms of service:
- Honour
robots.txtand per-host rate limits. - Use the GitHub API (with a token) rather than HTML scraping for bulk
pulls;
git clone --depth 1is the canonical bulk-fetch path. - Cache aggressively — the corpus changes a few times per week, not per minute.
- Do not impersonate browsers; identify your tool in the User-Agent string.
7.5 Recommended fork-and-mirror posture
For organisations that want long-term reliability rather than convenience, the safest posture is:
- Fork the repository into your own GitHub organisation.
- Mirror it to a private GitLab/Gitea/Bitbucket as well.
- Pin all dependencies (npm via lockfile already; Python via lockfile in your fork).
- Build the static site from your fork on your own CI; deploy from your own bucket/CDN.
- Sync from upstream on a deliberate schedule (weekly or fortnightly), re-running the lint chain before merging upstream changes.
This defends against upstream takedown, account compromise, and dependency drift simultaneously. It is the strongly recommended posture for any production deployment.
Important
The Unlicense permits unconditional copying. Use that latitude: mirror, fork, and build from your own copy. The upstream project is a single-maintainer effort and should not be treated as a dependency you can pin to.
8. Developer Internals
8.1 Repository structure deep-dive
HowToCook/
├── .github/
│ ├── readme-generate.js # Walks dishes/ and rebuilds README.md's index
│ └── manual_lint.js # Project-specific structural lint
├── dishes/ # Recipe corpus
│ ├── aquatic/ # Seafood
│ ├── breakfast/ # Breakfast items
│ ├── condiment/ # Sauces and seasonings
│ ├── dessert/ # Desserts and baked goods
│ ├── drink/ # Beverages and cocktails
│ ├── meat_dish/ # Meat-centred dishes
│ ├── semi-finished/ # Air-fryer / frozen products
│ ├── soup/ # Soups and congee
│ ├── staple/ # Rice, noodles, bread
│ ├── vegetable_dish/ # Vegetable-centred dishes
│ └── template/示例菜/ # Recipe template
├── tips/
│ ├── 厨房准备.md
│ ├── 如何选择现在吃什么.md
│ ├── 食材相克与禁忌.md
│ ├── learn/ # 11 fundamentals tutorials
│ └── advanced/ # 4 advanced-technique tutorials
├── starsystem/
│ ├── 0Star.md # Zero-difficulty
│ ├── 1Star.md # Single-pan
│ ├── 2Star.md # Basic stir-fry
│ ├── 3Star.md # Multi-step
│ ├── 4Star.md # Technique-driven
│ ├── 5Star.md # Hobbyist
│ ├── 7Star.md # Restaurant-grade
│ └── 8Star.md # Banquet menus
├── .gitattributes
├── .gitignore
├── .markdownlint.json
├── .textlintrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md # Auto-generated; do not edit by hand
├── package.json
├── package-lock.json
└── requirements.txt
8.2 The recipe template — dishes/template/示例菜/示例菜.md
Every contributed recipe is a copy of this file with the placeholder content replaced. The template's role is to keep the corpus parseable: every recipe has the same H2 sections in the same order, so downstream tools never need custom heuristics per recipe.
| Section | H-level | Required? | Purpose |
|---|---|---|---|
| Dish name | H1 | Yes | The dish, repeated as the file title |
必要原料 (necessary materials) |
H2 | Yes | Equipment and ingredients with quantities |
计算 (calculations) |
H2 | Yes | Per-serving math and scaling notes |
操作 (operations) |
H2 | Yes | Numbered preparation/cooking steps |
附加内容 (additional content) |
H2 | Optional | Variations, troubleshooting, finishing tips |
manual_lint.js (line-level details aside) confirms the H1 + the four H2
section headers are present. Missing any required section fails CI.
8.3 The README generator — .github/readme-generate.js
readme-generate.js walks dishes/ and rewrites the recipe-index sections
of README.md. The maintainer runs npm run build after merging any PR
that adds, removes, or renames a recipe. The script is idempotent: running
it twice in succession produces no diff.
Note
The README is therefore a derived artifact, not a hand-edited file. Pull requests that hand-edit README.md without running
npm run buildintroduce drift that the next maintainer will have to revert. Always regenerate after content changes.
8.4 The manual linter — .github/manual_lint.js
manual_lint.js performs three structural checks that markdownlint cannot:
- Template-section presence — every recipe must contain the four required H2 sections in the canonical order.
- Image-existence — every
reference in a recipe must resolve to a real file on disk. - Category placement — recipes must live in one of the eleven existing
dishes/<category>/folders, not at the root or in a typo'd folder.
Operators extending the linter should add new structural checks here rather
than overloading markdownlint with custom rules.
8.5 Code patterns and conventions
| Convention | Where it applies | Why |
|---|---|---|
| Chinese filenames | All recipes and tutorials | Authentic, search-friendly for the target audience |
| Same-name folder per dish (when images exist) | dishes/<cat>/<dish>/<dish>.md |
Keeps photos colocated with the recipe |
| Full-width punctuation in prose | Throughout | Standard Chinese typography; enforced by textlint |
| Half-width punctuation in code | All fenced code blocks | Standard programming syntax; enforced by inline-code escape |
Single master branch |
repository-wide | Documentation repos do not need GitFlow |
| Direct merges over rebases | Maintainer convention | Preserves contributor history |
8.6 Testing strategy
The "tests" for a recipe corpus are the linters and human review:
| Layer | Mechanism | What it catches |
|---|---|---|
| Structural | markdownlint-cli 0.41.0 |
Heading hierarchy, list integrity |
| Prose | textlint 12.x |
CJK spacing and bracket-width violations |
| Domain | manual_lint.js |
Template fields, image existence, category placement |
| Editorial | Human PR review | Recipe correctness, ingredient accuracy, photo quality |
There is no unit-test suite because there is no code under test.
8.7 Potential improvements
The following are tech-debt items and improvement opportunities visible from the codebase and the issue tracker:
- Pin Python dependencies in
requirements.txt— currently unpinned; exposes the static-site build to upstream regressions. Generate a lockfile viapip-compileoruv. - Add YAML frontmatter to recipes — issue #60 tracks this. Frontmatter would give downstream tools a structured ingredient list, prep time, servings, and difficulty without prose parsing.
- Address the
6Star.mdgap — the difficulty system jumps from 5 to 7;starsystem/6Star.mdis conspicuously absent. - Auto-generate per-day menus — issue #1751 requests this; it's a downstream-tool job that would benefit from frontmatter.
- Improve print legibility — issue
#1762 reports that
print output of recipes has tiny fonts. The fix is in the
mkdocs-with-pdfstylesheet, not the source recipes. - Annotation feature — issue #1758 requests inline annotations; this is a renderer feature, not a source change.
- Cuisine breadth — issue #1768 requests more Western dishes (Michelin-style); contributors welcome.
- Recipe-quality regressions — issue #1765 tracks the long-running quality discussion. A structured review pass on the twenty-most-edited recipes would close many leaf-level issues.
- Specific recipe corrections — issues #1757 (戚风蛋糕) and #1759 (南派 红烧肉) are open with detailed correction proposals; both are low-risk PRs for new contributors.
8.8 Contributing — the developer-facing process
For first-time contributors, the expected flow is:
- Open an issue describing the recipe or fix.
- Wait for maintainer triage (usually <72h).
- Fork, branch, edit, lint locally with
npm run lint. - Open a PR referencing the issue.
- Address review comments; CI must be green.
- Maintainer merges;
npm run buildruns to update README.
Maintainers should keep the merge cadence high but the bar firm: every PR
must pass npm run lint and have at least one finished-dish photograph
for new recipes. Editorial accuracy beats stylistic preferences.
9. Integration Points
The corpus is designed for downstream consumption. The following integration patterns are already in use or actively proposed by the community.
9.1 The cook.aiursoft.com mirror
The canonical downstream consumer. It performs the static-site build
described in Section 4 and serves a paginated, searchable, printable
rendering. Declared as homepage in package.json. Operationally
independent of the source repository.
9.2 GitHub-rendered tree
GitHub itself acts as a rendering layer — every .md file is browsable
with images inline. Use this for any low-stakes embedding via iframe or
direct linking.
9.3 Programmatic parsing — the stable Markdown contract
Downstream tools consuming the corpus rely on three stability guarantees:
| Guarantee | Maintained by | Failure mode if violated |
|---|---|---|
One recipe per .md file |
Authoring convention | Silent confusion in tools that count files |
Stable H2 section names (必要原料, 计算, 操作, 附加内容) |
manual_lint.js |
Parsers miss sections |
Images live next to the .md |
manual_lint.js |
Broken image links in renderers |
Tooling consuming the corpus should hard-fail (not silently degrade) when any guarantee is violated; otherwise stale or incorrect data leaks downstream.
9.4 Image consumers — caveat about external mirrors
Several community projects mirror or transform the recipe images. Operators of such mirrors should:
- Re-host images locally; do not hotlink to GitHub's user-content CDN.
- Respect the Unlicense (no attribution required, but politeness asks for it).
- Refresh on a schedule that matches the upstream commit cadence.
9.5 Proposed/future integrations
- NanoBananaPro image-rich preview + PDF export — issue #1755 proposes a third-party renderer optimised for visual-first browsing. Not yet implemented.
- YAML-frontmatter recipe schema — issue #60 proposes adding YAML frontmatter to recipes. If accepted, downstream tools would get a structured contract instead of relying on heading-text matching.
- Daily-menu generator — issue #1751 proposes automatic per-day menu generation. Best implemented as a downstream tool consuming the corpus.
9.6 Integration anti-patterns
Caution
Two integration patterns to avoid:
Don't depend on the README's recipe index for anything mechanical. The README is auto-generated by
readme-generate.jsand may drift between commits. Walkdishes/directly instead.Don't key downstream caches on the
package.jsonversionfield (1.5.0at this commit). It is not actively bumped on every content change. Use the git commit hash for cache invalidation.
9.7 Reaching the maintainers
| Channel | Best for |
|---|---|
| GitHub Issues (link) | Bug reports, feature requests, recipe corrections |
| GitHub Discussions (if enabled on your fork) | Open-ended conversation, Q&A |
| Pull Requests | Code, recipe, or documentation changes |
Repository email (in package.json author / git log) |
Operational concerns about the cook.aiursoft.com mirror |
Note
The project is maintained by a small team. Issue triage is best-effort and may take days for non-urgent items. Bug reports with reproducible minimal examples (a specific recipe path, a specific lint failure) get attention faster than open-ended discussion threads.