# HowToCook Administrator & Developer Guide

`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 `1857c9e` contains 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

```text
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.js` and the recipe
> template at `dishes/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

```text
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 `version` field reads `1.5.0` but 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

```json
{
    "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

```text
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:

```bash
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
  `mkdocs` use 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.json` or
  `.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:

```text
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/`, no `netlify.toml`, no `vercel.json` is 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](https://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

```bash
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

```bash
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 lint` against `master` to confirm CI is green.
- [ ] Run `npm run build` after merging any PR that adds or moves recipes.
- [ ] Review open `bug` issues — currently
  [#1757](https://github.com/Anduin2017/HowToCook/issues/1757),
  [#1759](https://github.com/Anduin2017/HowToCook/issues/1759), and
  [#1750](https://github.com/Anduin2017/HowToCook/issues/1750) (closed) —
  for actionable corrections.
- [ ] Confirm the static-site build still passes against the unpinned
  `requirements.txt` stack; pin versions in your fork if upstream breaks.
- [ ] Audit `dishes/template/示例菜/` against the linter; the template itself
  must pass `npm run lint`.

### 5.2 Linter operation reference

`npm run lint` is the single command operators run. Its semantics in detail:

```bash
npm run lint
```

Decomposes to:

```bash
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 `textlint` step 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 with `npm 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:

1. Create the new directory under `dishes/<new-category>/`.
2. Update `.github/manual_lint.js` if it hard-codes the allowed category list.
3. Update `.github/readme-generate.js` if it iterates a fixed category list.
4. Run `npm run lint` and `npm run build` to confirm no regression.
5. Add an entry to the README's category index by re-running `npm run build`.

> [!WARNING]
> Both `manual_lint.js` and `readme-generate.js` walk `dishes/` at runtime,
> but if either contains a hard-coded category whitelist, your new category
> will be silently dropped from the README index. Always run `npm run build`
> and 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 `master` and
  re-run `npm run build` to 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:
>
> 1. Cache `node_modules/` between CI runs — `npm install` is the longest
>    step and is fully deterministic against `package-lock.json`.
> 2. 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 audit` periodically; address high-severity advisories promptly.
- Pin Python dependencies in your fork's `requirements.txt` (upstream is
  unpinned). Consider switching to a lockfile via `pip-tools` or `uv`.

> [!CAUTION]
> The upstream `requirements.txt` does **not** pin versions. A forked
> static-site build can break overnight if `mkdocs-material`,
> `mkdocs-with-pdf`, or `weasyprint` releases an incompatible version.
> If you operate a production mirror, replace the bare requirements with
> a lockfile generated by `pip freeze` or `pip-compile` and 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.,
> `![](https://i.imgur.com/...)`), 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.txt` and per-host rate limits.
- Use the GitHub API (with a token) rather than HTML scraping for bulk
  pulls; `git clone --depth 1` is 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:

1. Fork the repository into your own GitHub organisation.
2. Mirror it to a private GitLab/Gitea/Bitbucket as well.
3. Pin all dependencies (npm via lockfile already; Python via lockfile in
   your fork).
4. Build the static site from your fork on your own CI; deploy from your
   own bucket/CDN.
5. 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

```text
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 build`
> introduce 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:

1. **Template-section presence** — every recipe must contain the four
   required H2 sections in the canonical order.
2. **Image-existence** — every `![](path)` reference in a recipe must
   resolve to a real file on disk.
3. **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 via `pip-compile` or `uv`.
- **Add YAML frontmatter to recipes** — issue
  [#60](https://github.com/Anduin2017/HowToCook/issues/60) tracks this.
  Frontmatter would give downstream tools a structured ingredient list,
  prep time, servings, and difficulty without prose parsing.
- **Address the `6Star.md` gap** — the difficulty system jumps from 5 to 7;
  `starsystem/6Star.md` is conspicuously absent.
- **Auto-generate per-day menus** — issue
  [#1751](https://github.com/Anduin2017/HowToCook/issues/1751) requests this;
  it's a downstream-tool job that would benefit from frontmatter.
- **Improve print legibility** — issue
  [#1762](https://github.com/Anduin2017/HowToCook/issues/1762) reports that
  print output of recipes has tiny fonts. The fix is in the `mkdocs-with-pdf`
  stylesheet, not the source recipes.
- **Annotation feature** — issue
  [#1758](https://github.com/Anduin2017/HowToCook/issues/1758) requests
  inline annotations; this is a renderer feature, not a source change.
- **Cuisine breadth** — issue
  [#1768](https://github.com/Anduin2017/HowToCook/issues/1768) requests more
  Western dishes (Michelin-style); contributors welcome.
- **Recipe-quality regressions** — issue
  [#1765](https://github.com/Anduin2017/HowToCook/issues/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](https://github.com/Anduin2017/HowToCook/issues/1757) (戚风蛋糕)
  and [#1759](https://github.com/Anduin2017/HowToCook/issues/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:

1. Open an issue describing the recipe or fix.
2. Wait for maintainer triage (usually <72h).
3. Fork, branch, edit, lint locally with `npm run lint`.
4. Open a PR referencing the issue.
5. Address review comments; CI must be green.
6. Maintainer merges; `npm run build` runs 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](https://github.com/Anduin2017/HowToCook/issues/1755) proposes a
  third-party renderer optimised for visual-first browsing. Not yet
  implemented.
- **YAML-frontmatter recipe schema** — issue
  [#60](https://github.com/Anduin2017/HowToCook/issues/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](https://github.com/Anduin2017/HowToCook/issues/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.js` and may drift between
> commits. Walk `dishes/` directly instead.
>
> **Don't** key downstream caches on the `package.json` `version` field
> (`1.5.0` at 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](https://github.com/Anduin2017/HowToCook/issues)) | 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.

---

_Generated April 2026 from Anduin2017/HowToCook at 1857c9e. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
