docs: sync certified-assets docs and make static-site the primary frontend path - #397
Conversation
Publishes the eight user-doc pages of dfinity/certified-assets as a synced tree under guides/frontends/static-site/, with the repo staying the single source of truth. Fetch-based rather than a submodule: the build resolves no file from that repo, only markdown links, so the pin is a ref in .sources/upstream.json under a new `synced` group and the pages are fetched at that ref. The script exits non-zero instead of publishing something broken: missing frontmatter, an absolute link to this site it cannot map, a relative link that does not resolve, or a banned character that survived normalization. The tree is deliberately not exempt from the validator. Pinned to a commit rather than the v0.3.3 tag: the only two commits past that release are the upstream docs PRs, and the tag predates the frontmatter contract this sync requires. Refs #396
Our Frontends pages taught @dfinity/asset-canister as the default while the templates, the icp-cli docs site, and this site all deploy @dfinity/static-site. Every configuration detail differs between the two, so the pages were not just out of date but wrong for anyone following them. - certification.md: every raw-access and automatic-behavior claim named one canister's answer as the platform's. Each claim now says which recipe it applies to. allow_raw_access stays documented; it is still correct for the asset canister, and certified-assets has no equivalent by design (certified-assets#123), so the static-site case is the gateway trust model instead. - asset-canister.md: reframed as the legacy path, moved last in the section, and given a migration section covering the reinstall requirement, the configuration mapping, and why per-file writes cannot come back. - frameworks.md, custom-domains.md, troubleshooting.md, internet-identity.mdx: _headers and _redirects first, .ic-assets.json5 kept as the legacy path. The II alternative-origins and app-metadata recipes need hand-written _headers rules, since static-site sets no headers at all. - application-canisters.md: adds the certified-assets interface, verified against its .did, and labels the asset canister section legacy. - project-structure.mdx: the templates ship static-site, and the recipe table was missing it. Refs #396
The sidebar showed "static-site", which is a recipe name rather than
anything a reader is looking for, because an autogenerated group takes its
label from the directory name. Frontends now lists its pages explicitly so
the group can be called "Hosting a static site"; the pages inside it stay
autogenerated, so a page added upstream still reaches the sidebar through
the sync alone.
One vocabulary, applied across the section: prose, headings, and the
sidebar name the goal ("a static site"), the recipe name appears where it
is typed (`@dfinity/static-site`), and the canister name appears only where
its identity matters (its Candid interface, state-hash verification, and
contrasting it with the legacy canister). Labels like
"static-site (certified-assets)" made a reader learn three names for one
thing; the rule is now written down in AGENTS.md.
Frontend frameworks now precedes custom domains. The frontmatter order
values stay in step with the explicit array because llms.txt still sorts
pages within a section by them.
Refs #396
The asset canister page linked only the word "canister" in its opening sentence, splitting the very term the page is about. Unlinked: the page has nowhere good to point for that name, since the canister's own source is poorly documented and its recipe is a different thing with a different name. Five links elsewhere still sent readers to that page for tasks that are not legacy: the architecture comparison table's frontend-hosting row, three next-steps entries, and a pointer to an agent setup example that has nothing to do with which canister serves the files. They now point at the static site guide, or at frameworks.md for the agent example. The troubleshooting entry splits in two, since `.ic-assets.json5` genuinely belongs to the legacy page. AGENTS.md gains the rule behind all of this: link the whole term, match the link to what the sentence is about, and keep recipe names pointing at recipe sources rather than at canister behavior. Refs #396
certified-assets#128 lands the retitle asked for in #127: the entry page is now "Deploy a static site", and its description no longer names the recipe and the canister in one sentence. Upstream also recorded the naming convention in its own rules file, and replaced the remaining icp0.io hostnames with icp.net. The sidebar entry under "Hosting a static site" therefore reads "Deploy a static site" with nothing left to override on our side, and one of our own Next steps entries that still used the old title as its link text now names the goal like the rest. The pin stays a commit: every commit past v0.3.3 is docs-only, so there is still no release that contains these pages. The bare cli.internetcomputer.org links the new lead introduces are left as upstream wrote them. That root meta-refreshes to the newest version, so it always lands on current docs, and AGENTS.md now records the exception so the next sync does not try to "fix" it. Refs #396
Three pages still sent a first-time reader to the legacy canister as the way to serve a frontend, which is the opposite of what the rest of this branch establishes: - guides/index.md, the landing page for every guide section, linked Frontends straight to asset-canister.md - choose-your-path.md said "Start with: Asset canister" for the web app path - calling-from-clients.md pointed there for "deploying the frontend that makes these calls" All three now point at the static site guide. Every remaining link to the legacy page is about the legacy path itself, and one of them was labeled "Asset canister guide" rather than naming it as legacy. No section landing page was added: guides/index.md already orients readers a level up, no other guides/ section has one, and a Frontends-only hub would be a third place to keep the same description in sync. Refs #396
…rammatic uploads as a pattern dfinity/examples removed hosting/photo-storage, because uploading assets from application code is no longer recommended and certified-assets does not support it at all. The link is gone, and both places that document AssetManager now say up front that it is specific to the legacy canister and not a pattern to adopt: an app with user-generated content keeps that content in a canister of its own. The API stays documented for projects already doing it. Refs #396
Stress-testing the static-site sync turned up three weaknesses. **Anchors were checked by nothing.** Our pages deep-link five headings inside the synced tree, and upstream can rename a heading whenever it likes, which would drop readers at the top of a long page with a clean build and a clean validator. `validate.js` now resolves the fragment of every relative `.md` link, repo-wide rather than only for the synced tree, since the same rot happens whenever anyone edits a heading. Three links were already broken: `#ic-assets-json5` (the real id is `ic-assetsjson5`), `cycle-costs.md#vetkd`, and `#reject-anonymous-callers`. Anchors are computed with `github-slugger`, the library Starlight itself uses, rather than a local approximation: comparing the two over all 2995 headings in docs/ showed 21 disagreements, on headings containing arrow, ampersand, and `/*`. It was already in the tree as a transitive dependency and is now declared, with the lockfile edited by hand to keep the entry a one-line diff. **A failing sync could leave a half-updated tree.** Pages were written inside the fetch loop, so a page violating the contract left earlier pages at the new ref and later ones at the old, a state that still builds and still validates. Every page is now transformed before any page is written. **A contract violation printed a stack trace.** Fail-loud only helps if the message is legible, so the failure paths now report one line and exit 1. Verified against a pre-contract ref (`--ref v0.3.3`, where no page has frontmatter): one line naming the file and the contract, exit 1, tree untouched. A normal run is unchanged and idempotent. Refs #396
"Asset canister" was still the generic name for the role across ten pages, which now points readers at the legacy path. Where a statement is true of both canisters, it names the role instead: the bullet introducing the two canisters an app starts with, the architecture comparison table (now "Frontend canister (certified responses)", matching "Backend canister (Wasm)"), the `ic_env` cookie in the client and Internet Identity guides, and the certification sentences in the concepts pages. Two were stale recommendations rather than naming: certified-variables told readers to use the asset canister for frontend assets, and service discoverability documented only the legacy `.well-known` configuration, which is also the fiddlier one. On a static site that file needs no configuration at all. Mentions that are genuinely about the legacy canister are untouched: the `.ic-assets.json5` references, and the SNS workflow in the governance guides, which uses that canister's `Prepare`/`Commit` roles and `propose_commit_batch`. Refs #396
Naming: "frontend canister" is the role and stays the default in prose about what the component does; "static site" is the task and is what link text uses; "certified-assets" names the canister only where its identity matters. Whatever noun a sentence needs, the link now goes to "Deploy a static site", the only page that documents how to get one. Added on first mention where a reader plausibly needs it, not on repeats, asides, or code comments. The rule is in AGENTS.md so the next page does not have to re-decide. The SNS exception: proposal-gated frontend updates are not possible on certified-assets, so telling readers to migrate everything was wrong. Verified against certified-assets.did at the pinned ref: its twenty methods include no proposal-gated commit and no staged-batch evidence, and its authorization is controllers plus a flat set of syncers who can all sync at will. An SNS could hold the controller with nothing for token holders to approve. So the legacy page now carries a "When to stay on this canister" section, its caution names the exception rather than saying everyone should migrate, the SNS asset-update workflow says which canister it requires, and the reference page records where the capability lives. Refs #396
When prose names certified-assets, it means the canister itself: its source, interface, and releases. That lives in dfinity/certified-assets, so the name links there, once per section rather than per mention, since both pages are entered through deep links. Recorded in AGENTS.md alongside the naming rule, including why it does not contradict "link internally when an internal page exists": our pages remain the target for the canister's behavior and its Candid surface. No issue filed upstream for proposal-gated updates; the topic is already being considered there, and the docs will drop the exception if it ships. Refs #396
A new page under guides/frontends/ needs a sidebar.mjs entry now that the group is explicit, and sidebar.order alone will not place it. The reason was only in a code comment, which is not where someone adding a page looks. Refs #396
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved workflow, synchronization, validation, and documentation issues remain.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR syncs certified-assets documentation and makes static-site the primary frontend path while retaining the legacy asset-canister path for SNS-governed updates.
Changes:
- Adds upstream pinning, synchronization, validation, provenance, and weekly automation.
- Adds eight static-site documentation pages and reorganizes frontend navigation.
- Updates frontend, security, authentication, governance, testing, and getting-started guidance.
- Adds repository-wide anchor validation and naming rules.
File summaries
| File | Reviewed changes |
|---|---|
sidebar.mjs |
Adds static-site navigation grouping. |
scripts/validate.js |
Adds repository-wide anchor validation. |
scripts/sync-static-site.mjs |
Fetches, transforms, and validates upstream pages. |
package.json |
Adds sync tooling and slugger dependency. |
package-lock.json |
Locks dependency metadata. |
docs/references/application-canisters.md |
Documents frontend canister options. |
docs/guides/testing/strategies.md |
Updates frontend testing terminology. |
docs/guides/security/data-integrity-and-authenticity.md |
Updates frontend integrity guidance. |
docs/guides/security/canister-control.md |
Generalizes frontend canister terminology. |
docs/guides/index.md |
Makes static-site the frontend entry point. |
docs/guides/governance/managing.md |
Documents the legacy governance workflow. |
docs/guides/frontends/static-site/verifying-contents.md |
Adds state-hash verification guidance. |
docs/guides/frontends/static-site/site-files.md |
Documents uploaded and skipped files. |
docs/guides/frontends/static-site/routing.md |
Documents routing and clean URLs. |
docs/guides/frontends/static-site/redirects.md |
Documents redirect and rewrite rules. |
docs/guides/frontends/static-site/overview.md |
Adds the static-site deployment guide. |
docs/guides/frontends/static-site/how-it-works.md |
Explains certification and serving behavior. |
docs/guides/frontends/static-site/headers.md |
Documents custom headers. |
docs/guides/frontends/static-site/access-protection.md |
Documents access protection. |
docs/guides/frontends/service-discoverability.md |
Updates service discovery guidance. |
docs/guides/frontends/frameworks.md |
Teaches static-site framework deployment. |
docs/guides/frontends/custom-domains.md |
Updates custom-domain guidance. |
docs/guides/frontends/certification.md |
Contrasts certification behavior. |
docs/guides/frontends/asset-canister.md |
Reframes the asset canister as a legacy option. |
docs/guides/canister-management/troubleshooting.md |
Updates frontend troubleshooting. |
docs/guides/canister-calls/calling-from-clients.md |
Updates frontend hosting examples. |
docs/guides/backends/certified-variables.md |
Updates frontend certification links. |
docs/guides/authentication/internet-identity.mdx |
Adds static-site header configuration. |
docs/getting-started/quickstart.md |
Uses static-site in the quickstart. |
docs/getting-started/project-structure.mdx |
Updates project recipe examples. |
docs/getting-started/choose-your-path.md |
Makes static-site the recommended path. |
docs/getting-started/app-architecture.md |
Updates frontend architecture guidance. |
docs/developer-tools/index.md |
Updates recipe terminology. |
docs/concepts/vetkeys.md |
Corrects a cycle-cost anchor. |
docs/concepts/security.md |
Corrects an access-management anchor. |
docs/concepts/edge-infrastructure.md |
Updates frontend certification references. |
docs/concepts/certified-data.md |
Updates certified-assets terminology. |
AGENTS.md |
Documents synced trees and naming rules. |
.sources/upstream.json |
Adds the certified-assets sync pin. |
.github/workflows/sync-static-site.yml |
Automates documentation synchronization. |
.agents/upstream-tracking.md |
Documents synced-tree maintenance. |
Review details
Suppressed comments (7)
.sources/upstream.json:63
- The automated workflow updates only
entry.pinned. After the first release bump, this$commentwill still say the current pin is a commit past the latest release, so the source manifest will carry false operational guidance. Make this wording historical or update the comment as part of the bump.
"Pinned to a commit, not a release tag, and past the latest release.",
".sources/VERSIONS forbids pinning past a release so that docs cannot",
"describe unshipped behavior; this is the case that rule is not aimed at.",
AGENTS.md:220
- After adding the synced and reference sections, this says upstream repos are tracked three ways, but this document now defines four: vendored, synced, watched, and reference (also recorded as four in
.sources/upstream.json). This misdirects contributors about which source procedure applies. Change “three” to “four”.
Upstream repos are tracked three ways. Which one decides where you read from.
docs/guides/frontends/static-site/verifying-contents.md:54
- This example is presented as command output but is not a verifiable output: the hash contains an ellipsis while the comment claims 64 hexadecimal characters. The documentation rules prohibit invented output, so replace it with output copied from an upstream fixture or mark it explicitly as needing verification.
# 8150a65e854b9bbb… (64 hex chars, and nothing else)
docs/guides/frontends/static-site/verifying-contents.md:69
- This second example also presents invented command output: the blob value is truncated with an ellipsis, so readers cannot use it to reproduce or compare the result. Replace it with output copied from an upstream fixture or mark it explicitly as needing verification.
# (blob "\81\50\a6\5e…")
docs/guides/index.md:18
- The generic 404 page still directs users to
/guides/frontends/asset-canister/as the Frontends entry point, so this PR leaves a prominent navigation path on the legacy recipe despite changing the guide index to static-site. Updatedocs/404.mdxto point at the static-site overview and use the new description.
scripts/sync-static-site.mjs:160 brokenLinksdiscards the fragment and only checks the file path. Becausevalidate.jsskips synced files, a future upstream heading rename can leave these generated deep links broken without failing the sync. Validate fragments against the generated target, using the same slugger, before accepting the tree.
scripts/sync-static-site.mjs:94- This only distinguishes fenced blocks, so
fnalso receives inline code, link destinations, and frontmatter. The promised prose-only normalization can therefore rewrite a future identifier or URL, such astamper-proof, and break it. Protect those regions or use a Markdown-aware transform.
- Files reviewed: 40/41 changed files
- Comments generated: 7
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Fourteen findings, seven posted and seven suppressed. The substantive ones: **A synced page published a `dfx` command.** dfx is banned here, and the ban was never enforced anywhere, so it shipped. `icp canister call` takes a principal and `-e` selects the network (checked against cli.md at v1.5.0), so the sync now rewrites that exact shape and reports it, and refuses to publish any other `dfx` occurrence rather than guessing at a translation. **Same-page anchors were never checked.** The fragment check skipped `#`-only hrefs, so `(#section)` links could rot freely. Four were already broken, in the glossary and the Candid spec. **The atomicity guarantee did not hold.** Link and banned-character checks ran after writing, so a failure left the tree modified while claiming otherwise. All checks now run against the in-memory pages, and those checks include link fragments, resolved against the pages about to be written. The anchor logic is now one module shared with the validator instead of two implementations. **Prose normalization could reach into code.** It skipped fenced blocks but not inline code, link destinations, or frontmatter, so a future `tamper-proof` identifier or URL could have been rewritten. Unit-tested against all four. **The workflow interpolated upstream filenames into shell source.** They now arrive as environment values, so nothing upstream writes can become script. Also: the manifest `$comment` described the current pin, which the workflow rewrites, so it stated a rule instead; `AGENTS.md` said upstream repos are tracked three ways when it defines four, and its own table used the mixed label the naming rule forbids; and the 404 page still sent readers to the legacy canister as the Frontends entry point. Not taken: extending the validator to ban `dfx` repo-wide. The SNS guides reference it deliberately, with a note that `icp-cli` has no equivalent for the `dfx sns` extension, so that needs an exemption mechanism and its own change. Refs #396
|
Copilot's review had seven posted comments and seven suppressed ones. The posted ones are answered and resolved in their threads. The suppressed ones, since they have none: Fixed in c081394
Not fixed, deliberately
Not taken
|
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate findings affect synchronization, validation, workflow behavior, and source metadata.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (10)
Previously missed (1) — in code that hasn't changed since the last review.
docs/guides/frontends/static-site/verifying-contents.md:55
- This shows a synthetic, truncated hash as command output, but
state-hashdepends on the reader's completedisttree and the preceding steps provide no fixture that could produce this value. Readers cannot reproduce8150a65e..., so this should be removed or replaced with output copied from a real upstream fixture before regenerating the synced page.
.github/workflows/sync-static-site.yml:64
- When a new release is a descendant of the pinned docs-only commit but has no additional changes under
docs/, this empty-CHANGEDbranch skips the update. The pin then remains at the commit instead of moving back to the release tag, contrary to the documented policy and leaving future syncs on a non-release ref. Treat a descendant tag with an empty docs diff asneeded=trueso the manifest pin is advanced (the regenerated pages can otherwise be unchanged).
CHANGED=$(git -C /tmp/certified-assets diff --name-only "${PIN}..${TAG}" -- docs/)
if [ -z "$CHANGED" ]; then
echo "No docs/ changes between $PIN and $TAG. Skipping."
echo "needed=false" >> $GITHUB_OUTPUT
else
.sources/upstream.json:58
- The manifest pins
65c0f32, but the PR description and this entry's rationale still say the sync was bootstrapped atd9cb7df. Since the generated pages also record65c0f32, the reproducibility record is inconsistent; update the description and$commentto the actual pin, or reset the pin and generated provenance to the stated bootstrap ref.
"repo": "dfinity/certified-assets",
"pinned": "65c0f32",
"source": "docs/",
"target": "docs/guides/frontends/static-site/",
"script": "scripts/sync-static-site.mjs",
AGENTS.md:249
- The new synced-tree model is not reflected in
CONTRIBUTING.md: it still says upstream repos are tracked in two ways and omitsdocs/guides/frontends/static-site/from the synced-content list. Contributors following the repository-level guide will therefore receive contradictory instructions about where these pages come from and whether they may edit them. Update the contributor guide in this PR.
| Topic | Repo | Synced tree |
|-------|------|-------------|
| certified-assets canister behavior | `dfinity/certified-assets` | `docs/guides/frontends/static-site/` |
docs/references/application-canisters.md:12
- This sentence says there is “no uncertified serving path,” but the static-site guidance documents
<canister-id>.raw.icp.netas a gateway that forwards the certified response without checking its proof. The canister attaches a certificate, but visitors on that raw host do not receive a verified response; qualify the claim with the gateway distinction so readers do not treat raw URLs as integrity-protected.
scripts/sync-static-site.mjs:140 - The lookup uses the full canonical URL as its key, so a mapped page URL with a fragment (for example,
.../guides/frontends/custom-domains#...) is treated as unmapped even though this table is documented as acceptingpath#fragment. The next upstream link of that form will fail the sync; split the fragment before the lookup and append it to the mapped relative target.
scripts/sync-static-site.mjs:67 - This pattern does not match the exact retired URL
https://internetcomputer.org/docsbecause it requires another slash and a nonempty path after the optional/docs. That URL would survive the rewrite and the synced-tree validators would publish the retired link, despite the comment saying the retired/docsform is supported. Add an explicit end-of-URL case while continuing to exclude the bare domain.
scripts/sync-static-site.mjs:186 - When upstream removes a page, it is absent from
prepared, but this fallback still accepts the stale copy currently on disk. The later cleanup then deletes that copy, leaving a dangling link that the pre-write validation incorrectly approved. Treat paths insideTARGET_DIRas valid only when their relative filename exists inprepared; use the on-disk fallback only for pages outside the synced tree.
scripts/sync-static-site.mjs:335 - The final sync phase writes directly into the target and then deletes stale files. An interruption or I/O error after one write can therefore leave a mixed checkout containing pages from two refs; a local rerun or subsequent tooling can observe that partial tree. Stage the complete output in a temporary directory and replace the target only after every write succeeds.
scripts/validate.js:93 - The new repo-wide anchor check still treats any href beginning with
httpas an external URL. That skips relative files such ashttp-gateway-protocol-spec.md, and a futurehttp-gateway-protocol-spec.md#...link would bypass fragment validation entirely. Restrict this condition to actual URL schemes so filenames beginning withhttpare checked.
- Files reviewed: 44/45 changed files
- Comments generated: 2
- Review effort level: Lite
Twelve findings, two posted and ten suppressed. The ones that could produce
wrong output:
**The dfx rewrite could publish a broken command.** `--network ic` was
optional in the pattern, so `dfx canister call foo bar --network local` would
have had its prefix rewritten and kept its flag, yielding `icp ... -e ic
--network local`, which conflicts, and the dfx check could no longer catch it
because the word was gone. The pattern now matches one complete supported
command anchored to the line, and every other shape falls through to the
failure path. Unit-tested over four shapes.
**The validator skipped relative files beginning with "http".** It tested
`href.startsWith('http')`, so `http-gateway-protocol-spec.md`, which exists
and is linked, was treated as an external URL and checked for nothing. Now
matched as a URL scheme.
**A page removed upstream could leave an approved dangling link.** The
pre-write link check fell back to the copy on disk, which the swap then
deletes. Inside the tree, `prepared` is now the only truth.
**The retired `internetcomputer.org/docs` URL with no path escaped the
rewrite** and would have been published as-is.
**A mapped page reached with a fragment counted as unmapped**, so the next
upstream link of that shape would have failed the sync even though the map is
documented as accepting `path#fragment`. The fragment now carries over.
**A release with no docs changes stranded the pin on a commit.** The workflow
skipped when `docs/` was unchanged, so the pin would never have moved back onto
a release tag, which is the whole plan for it. It now opens the PR either way
and says which of the two it is.
**The write was not atomic.** Pages are staged outside `docs/` (a staging
directory left inside it would be built as content) and swapped in by rename,
which also subsumes deleting pages that upstream removed.
Also: the manifest `$comment` read as a description of the current pin rather
than the policy; the `static-site` watched entry still said these pages were
"not yet documented"; `CONTRIBUTING.md` said upstream repos are tracked two
ways and omitted the tree from its synced list; and the reference page claimed
there is "no uncertified serving path" without the gateway distinction that
makes a raw URL unverified for the visitor.
Left for upstream: the elided `state-hash` output, which the maintainer is
handling in certified-assets.
Refs #396
|
Second Copilot review: two posted comments, answered and resolved in their threads, and ten suppressed. The suppressed ones, all in 38366ba except where noted: Fixed, and these could have produced wrong output
Fixed, documentation consistency
Left for upstream
|
There was a problem hiding this comment.
🟡 Changes recommended
The review includes one critical workflow issue and several moderate documentation, synchronization, and anchor-validation issues.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (9)
.github/workflows/sync-static-site.yml:61
- The PR description says this workflow opens a bump PR only when
docs/changes, but this branch deliberately setsneeded=trueand opens a pin-only PR when a release contains no documentation changes. Reconcile the stated contract with the implementation, either by skipping that PR or documenting the pin-only release PR behavior.
# A release that ships canister changes without touching docs/ leaves
# the synced pages byte-identical, so there is no content to review.
# The pin still has to move: it is allowed to sit on a commit only
# while no release carries the pages, and skipping here would strand
# it on that commit for good. So the PR is opened either way, and the
# body says which of the two it is.
docs/404.mdx:16
- This newly updated internal link uses a root-relative URL and a trailing slash, but the repository's linking rule requires relative
.mdlinks. Useguides/frontends/static-site/overview.mdso the validator and source links can resolve the target consistently.
- **[Frontends](/guides/frontends/static-site/overview/)**: Host a static site, integrate frameworks, configure custom domains, and certify responses.
docs/concepts/certified-data.md:45
- This now links the generic certified-assets explanation to the static-site canister, but the claim that the HTTP gateway verifies every browser response is too broad: the same canister also exposes
rawhostnames where the gateway deliberately skips verification. Scope this sentence to a verifying hostname/gateway so readers do not mistake certification by the canister for verification of every URL.
- **Certified assets.** A [frontend canister](../guides/frontends/static-site/overview.md) uses certified variables to produce certified HTTP responses. When a browser fetches a page served by an ICP canister, the HTTP gateway verifies the certificate before serving the response, so the browser sees only content that was committed through consensus.
docs/getting-started/project-structure.mdx:121
- This page now presents
@dfinity/static-siteas the frontend recipe, but the project-tree example above still labelsfrontend/canister.yamlas “Asset canister configuration.” Readers copying the documented layout are sent toward a legacy/nonmatching configuration file; update that tree to the static-site layout or explicitly mark it as legacy.
The frontend uses the `@dfinity/static-site` recipe, which builds the frontend app and uploads the output to a canister that serves it with response certification:
docs/guides/canister-management/troubleshooting.md:103
- The static-site canister does add managed response headers such as
Content-Type,ETag, certification headers, andic_env; the syncedstatic-site/headers.mddocuments these explicitly. The relevant troubleshooting distinction is that it adds no default security headers or CSP, so saying it sets "no headers of its own" is factually too broad. Narrow this sentence to default security headers/CSP.
**On a [static site](../frontends/static-site/overview.md)** the canister sets no headers of its own, so a CSP error means your own policy is missing or not matching. `_headers` patterns match the file that was served, not the URL the visitor asked for, so a rule written against a client route (`/dashboard/*`) matches nothing. Write it against the file instead (`/index.html` or `/*.html`), redeploy, and check the response:
docs/guides/frontends/certification.md:205
- This sentence contradicts the raw-host behavior documented above: both recipes can be reached through a
rawhostname, where the gateway forwards responses without checking the certificate. Limit the transparent-verification claim to requests sent through a verifying hostname/gateway; otherwise readers may treat a raw URL as authenticated.
For standard asset serving through either frontend recipe, verification is transparent: the boundary node verifies every response before forwarding it to the browser, and you do not need any JavaScript verification code.
docs/guides/security/data-integrity-and-authenticity.md:626
- This recommendation now links to the static-site path, but the next bullet still tells readers to inspect
http_requestand reject raw-host requests. The synced static-site documentation explicitly says this canister cannot reliably distinguish or refuse raw hosts becauseHostis unauthenticated; following this guidance therefore asks static-site users to implement a capability the canister does not have. Scope thehttp_requestrejection to custom HTTP/legacy asset-canister implementations and direct static-site users to a verifying gateway instead.
scripts/lib/anchors.mjs:37 - The heading-ID plugin supports both
{#id}and{$id}forms, but this anchor index only recognizes the#form. Any link to a heading using the portal's{$...}syntax will be reported as broken even though the build assigns that explicit ID; use the same[#$]recognition here.
scripts/sync-static-site.mjs:250 sourcePages()only lists the immediatedocs/directory and filters to file entries, so a future upstream page under a nested directory is silently omitted even though the release check will open a sync PR. That contradicts the comment that newly added pages reach readers automatically; recurse through directory entries (and create matching staging parents) or fail explicitly when nested pages are present.
- Files reviewed: 45/47 changed files
- Comments generated: 2
- Review effort level: Lite
Eleven findings, two posted and nine suppressed. One was critical.
**The workflow's build check would have failed on its first run.** It ran
`git submodule update --init` without the SSH-to-HTTPS rewrite that
`.gitmodules` requires on a runner, and it omitted `.sources/motoko`, which
the build needs because pages under docs/languages/motoko/ pull code through
`file=<motokoExamples>/...` includes. Without it they render empty *without
failing the build*, so the step would have reported a passing build for a site
with empty Motoko pages. Now identical to build.yml.
**The anchor index recognized fewer explicit ids than the build assigns.**
`plugins/remark-heading-id.mjs` accepts `{#id}` and `{$id}`; the index accepted
only the first, and matched a looser pattern besides, so it could have called a
working link broken. The plugin now exports its pattern and the index uses it,
so they cannot diverge.
**A nested upstream page would have been dropped silently** while the release
check kept reporting the range as synced. Subdirectories now stop the sync with
what to decide.
Five findings were the same class of imprecision in our prose, all worth
fixing: the canister certifying every response is not the same claim as a
visitor receiving a verified one, which depends on the gateway their URL points
at. `certified-data.md`, `certification.md` and the security checklist now say
so, and the checklist no longer asks static-site readers to refuse raw hosts in
`http_request`, which that canister cannot do. Also: the static-site canister
does manage some headers (`Content-Type`, `ETag`, certification, `ic_env`), so
"no headers of its own" became "no default security headers and no CSP"; and
the project tree still labelled `canister.yaml` as asset canister config.
The documented pin procedure and the PR description both still said a release
with no `docs/` changes produces nothing, which the previous commit made false.
Both now describe the pin-only PR.
Not accepted: making `docs/404.mdx` use relative `.md` links. Every link on
that page is root-relative because it has to be: a 404 is served at whatever
URL the reader typed, so a relative link would resolve against a path that does
not exist. The rule it cites is for pages with a stable location.
Refs #396
|
Third Copilot review: two posted comments, answered and resolved in their threads, and nine suppressed. All in 72601ca. Fixed, would have broken or misled
Fixed, the same class of imprecision in our prose The canister certifying every response is not the same claim as a visitor receiving a verified one, which depends on the gateway their URL points at. Four findings were versions of that, and all four were right:
Fixed, my own drift
Not accepted
|
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical sync pagination and additional moderate workflow, documentation, and generated-content issues block approval.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (14)
Previously missed (2) — in code that hasn't changed since the last review.
CONTRIBUTING.md:111
- This contributor guide says upstreams are tracked three ways and that everything else is watched, but
.sources/upstream.jsonand.agents/upstream-tracking.mddefine a fourthreferencegroup with no pin and no release issue. That makes the documented verification and maintenance procedure incorrect for the reference repositories. Please describe all four groups here and replace the ambiguous “Bumping either” guidance with the actual sync and pin procedures.
scripts/validate.js:121 - This cross-page variant repeats the malformed
no heading ... slugs towording introduced by the anchor validation. Use the same clearhas slugwording as the same-page diagnostic so CI identifies the missing heading consistently.
.github/workflows/sync-static-site.yml:54
- The workflow treats any existing remote branch as proof that an open PR exists. If the push succeeds but
gh pr createfails, every later run takes this branch and exits, leaving the release permanently unsynced with no PR. Check for an actual open PR or clean up and reuse orphaned branches before skipping.
BRANCH="infra/sync-static-site-${TAG}"
if git ls-remote --exit-code origin "refs/heads/${BRANCH}" > /dev/null 2>&1; then
echo "Branch $BRANCH already exists, so a PR is likely open. Skipping."
echo "needed=false" >> $GITHUB_OUTPUT
exit 0
fi
CONTRIBUTING.md:108
- This newly added prose line uses an em dash as punctuation, contrary to the repository's explicit rule in
AGENTS.md:182and the validator's copy conventions. Replace the separator with a colon or semicolon.
- **Synced without a submodule** — a repo whose own pages are published here, pinned under `synced` in [`.sources/upstream.json`](.sources/upstream.json). Nothing is resolved at build time, so there is no submodule to hold the pin. Its own weekly workflow opens the bump PR.
docs/guides/frontends/certification.md:49
- The blanket
Any canisterclaim treats.icp.netand.raw.icp.netas universal hostnames, but the synced trust model says hostname verification is a property of how a particular gateway is deployed. Custom gateways need not expose either hostname, so this also undercuts the following static-site qualification. Scope the table to the standard ICP gateway or say that a gateway may expose these host types.
Any canister that serves HTTP is reachable through two kinds of hostname: one where the gateway verifies the response certificate before forwarding it, and a `raw` one where it forwards the response without checking:
| Domain | Certification | Notes |
|--------|--------------|-------|
| `<canister-id>.icp.net` | Verified | The gateway checks the proof on every response |
docs/guides/frontends/static-site/access-protection.md:37
- These commands use the default local environment, but the next paragraph promises an
icp.netURL. Following this quick start therefore deploys and configures a local canister while directing the reader to a mainnet hostname. The repository documentsicp deploywithout an environment as local andicp deploy -e icas mainnet indocs/guides/canister-management/lifecycle.mdx:82-84; add-e icto all three commands for this mainnet example, or change the URL and instructions to describe local access. Because this page is generated, apply the change in the certified-assets source before regenerating it.
icp canister call frontend enable_protection '("/login.html")'
# 3. Mint a credential. Here, a chosen "password" valid for ~1 year.
icp canister call frontend issue_token \
'(record { label = "owner"; ttl_secs = 31536000 : nat32; value = opt "my-passphrase" })'
docs/guides/frontends/static-site/how-it-works.md:10
- This generated page names the
certified-assetscanister without linking to its upstream repository, while the new naming rule inAGENTS.md:398requires the canister name to link there (the overview link is about the task, not the canister). Update the source sentence to linkcertified-assetstohttps://github.com/dfinity/certified-assets, then regenerate rather than editing this file.
You don't need any of this to use certified-assets; the [overview](overview.md) is
docs/guides/frontends/static-site/overview.md:63
- This says the sync uploads and certifies every file in
dir, but the synced site-files reference explicitly says_headersand_redirectsare configuration and are never uploaded as assets. Clarify that the plugin processes those files as configuration and uploads the remaining content files, then fix the upstream source and regenerate this page.
This installs the canister, then runs the sync plugin to upload and certify every
file in `dir`. Re-running `icp deploy` syncs again: the plugin diffs your directory
against the canister and uploads only what changed.
docs/guides/frontends/static-site/site-files.md:17
- The absolute wording here contradicts the next section:
_headersand_redirectsare explicitly not uploaded or served as assets. Please change this to say ordinary files are uploaded and served, while the two special files configure behavior, and apply the correction upstream before regenerating the page.
docs/guides/frontends/static-site/verifying-contents.md:54 - This generated example is not valid command output: it contains a Unicode ellipsis while claiming the tool prints 64 hexadecimal characters and nothing else. The repository's documentation rules prohibit invented output, so update
certified-assets's source page with a real fixture or an explicit verification TODO, then regenerate this page.
docs/guides/frontends/static-site/verifying-contents.md:69 - This second command block also presents fabricated output (
…) rather than a copyable result. Since the exact Candid rendering is not verified, remove the sample or replace it with a fixture-backed output (or an explicit verification TODO) in the upstream page before syncing.
scripts/sync-static-site.mjs:297 - This directory is created before fetching, transforming, and validating the pages. On a checkout where the generated target does not yet exist, any later failure leaves an empty
docs/guides/frontends/static-site/directory even though the error path reports that nothing was written and the comment says the tree remains unchanged. Defer creation until the staging swap; the later.sync-stagingsetup is sufficient.
scripts/sync-static-site.mjs:223 - The check claims to validate
sidebar.order, but/^\s+order:/maccepts anordernested under any unrelated frontmatter key. A future upstream page could therefore pass this contract without asidebar.orderand sort arbitrarily in the generated group. Parse or scope the check to thesidebarmapping before publishing the page.
scripts/sync-static-site.mjs:7 - This description says the sync performs no prose rewrites, but the same script deliberately normalizes punctuation and spelling and rewrites the supported
dfxcommand below. That makes the contract misleading for anyone deciding whether a source change can be safely synced. Qualify this statement to say that only the listed mechanical transformations are applied.
- Files reviewed: 46/48 changed files
- Comments generated: 1
- Review effort level: Lite
Fifteen findings. Eight were ours and are fixed here; six are upstream prose in the synced tree and are the source repo's call; one is wrong. **The workflow could strand a release forever.** It treated any existing remote branch as proof of an open PR, so a run that pushed and then failed at `gh pr create` would have made every later run skip that release, with no PR and no further attempts. It now skips only on an actually open PR, and deletes an orphaned branch so the next run retries. **Sync contract holes:** - The frontmatter check accepted `order:` nested under any key, so a future page could pass the contract without `sidebar.order` and sort arbitrarily. Scoped to the `sidebar` mapping. - The target directory was created before fetching, so on a fresh checkout a failure left an empty directory behind while reporting that nothing was written. The staging swap creates it now. - The header comment claimed nothing is rewritten, which stopped being true when punctuation normalization and the `dfx` rewrite arrived. It now says only the listed mechanical transformations are applied. - Added a guard for the contents API's 1000-entry cap, which is the real version of the reported pagination problem (see below). **Docs shape:** - `CONTRIBUTING.md` described three tracking groups when there are four: the `reference` group, with no pin and no release issue, was missing, and "bumping either" no longer matched three procedures. Also dropped the em dashes I had added there. - `certification.md` claimed "any canister" is reachable on both hostname kinds, which overstates it: which hostnames exist, and which verify, is a property of the gateway deployment. Scoped to the standard ICP gateway. - Both anchor diagnostics now read "X has no heading with slug Y" instead of the awkward "no heading in X slugs to Y". Not accepted: the reported critical finding, that the contents API returns 30 entries and needs pagination. Tested against a real directory: 73 entries in a single response and no Link header, because that endpoint is not paginated. The documented limit is 1000 entries, which is now guarded. Refs #396
|
Fourth Copilot review: one posted comment, answered and resolved in its thread, and fourteen suppressed. Eight were ours and are fixed in 32b9bd4; six are upstream prose in the synced tree and belong to the source repo; one is wrong. Fixed, sync logic
Fixed, docs shape
Upstream, not ours to change Six findings are about the prose in the synced pages: the Not accepted
|
There was a problem hiding this comment.
🟡 Changes recommended
An unresolved critical network-targeting issue and additional moderate and nit findings must be addressed.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (10)
.github/workflows/sync-static-site.yml:38
- The tag selector also accepts prereleases such as
v0.4.0-rc.1, so a prerelease would be treated as the latest release and the workflow could publish documentation from unreleased behavior. This should use the same exact stable-tag pattern as theupstream.jsonwatcher (^v\d+\.\d+\.\d+$) before advancing the pin.
TAG=$(git -C /tmp/certified-assets tag --sort=-version:refname | grep '^v[0-9]' | head -1)
docs/getting-started/quickstart.md:112
- The label defines the frontend-canister role as if it were the static-site task. The naming rule added in this PR distinguishes those concepts, so describe what the canister does instead of equating the two.
2. **Frontend canister**: A [static site](../guides/frontends/static-site/overview.md) that serves your React app. It automatically provides the backend's canister ID to your frontend code via a cookie, so the two canisters can communicate without manual configuration.
docs/guides/frontends/asset-canister.md:363
- This sentence refers to the recipe, but the new naming rule reserves the bare
static-siteform for neither a reader-facing task nor an explicit recipe name. Use@dfinity/static-sitehere so readers can distinguish the recipe from the canister and the static-site task.
Also drop any `configuration.version` field: with static-site the recipe version is the canister version.
docs/guides/frontends/asset-canister.md:369
- This sentence describes the hosting task, not the recipe, so the bare hyphenated
static-siteconflicts with the naming rule added in this PR. Saya static sitehere, reserving@dfinity/static-sitefor the recipe name.
If your app stores user-generated content, keep serving the frontend from static-site and store uploads in a separate canister that the frontend calls. Mixing a mutable file store into your deploy target means every user upload changes what your site is.
docs/guides/frontends/static-site/verifying-contents.md:54
- This synced page presents an ellipsized SHA as command output, but the repository rule requires output to come from an upstream fixture or be explicitly marked for verification. Please remove this sample or add the verification TODO in the upstream page and regenerate the synced tree; do not hand-edit this generated file.
docs/guides/frontends/static-site/verifying-contents.md:69 - This synced page presents an ellipsized
icp canister callreply as command output, but the repository rule requires output to come from an upstream fixture or be explicitly marked for verification. Please remove this sample or add the verification TODO in the upstream page and regenerate the synced tree; do not hand-edit this generated file.
docs/guides/frontends/static-site/verifying-contents.md:68 - This generated example invokes
state_hashwithout an explicit Candid argument. The static-site docs themselves note thaticp canister callopens an interactive prompt when the argument is omitted, so this does not perform the documented non-interactive read for a no-argument method. Please fix the upstream page and resync it asstate_hash '()' -e ic.
docs/guides/security/data-integrity-and-authenticity.md:626 - This recommendation tells readers to inspect
http_requeston the legacy asset canister, but that is a prebuilt canister and users cannot change its handler. The legacy path is configured withallow_raw_access: falsein.ic-assets.json5; reserve thehttp_requestcheck for custom HTTP canisters so the documented remediation is actionable.
scripts/sync-static-site.mjs:386 - If the process is terminated after the old tree is renamed at line 384 but before line 385 completes,
docs/guides/frontends/static-site/is absent and the previous pages are left only under ignored scratch space. A canceled workflow or interrupted local sync then cannot build until the sync is rerun; add a rollback/cleanup path or another atomic replacement strategy for this interruption window.
scripts/sync-static-site.mjs:25 - The generator copies certified-assets documentation into this repository but only adds provenance and a do-not-edit marker; it does not carry the upstream license alongside the generated files or add an attribution entry to
NOTICE. That conflicts with the repository's explicit copied-content policy in.agents/upstream-tracking.md:41-44, so add the license/notice handling to the sync design before publishing this tree.
- Files reviewed: 46/48 changed files
- Comments generated: 1
- Review effort level: Lite
Eleven findings. Six were ours and are fixed here; three are upstream prose;
one is a policy question for the maintainer; one I extended to a second
instance it missed.
**The command we publish was subtly wrong.** The dfx rewrite produced
`icp canister call <id> state_hash -e ic`, and `icp canister call` with no
argument opens an interactive prompt rather than sending an empty one. dfx
infers `()`; icp does not. The rewrite now emits `state_hash '()' -e ic`.
**A prerelease could have advanced the pin.** The workflow selected tags with
`^v[0-9]`, so `v0.4.0-rc.1` would have counted as the latest release and
published docs for unreleased behavior, which is the exact thing the pin policy
exists to prevent. Now the same stable-only pattern the upstream.json watcher
uses.
**Two migration commands targeted the local replica.** The `authorize` call and
the reinstall in "Migrate to a static site" both run against an already
deployed canister, so without `-e ic` they would have hit local and silently
done nothing useful. The review flagged the first; the second is the same
mistake four lines up.
**A remediation was not actionable on the canister it named.** The security
checklist told readers to check `http_request` on the legacy asset canister,
which is prebuilt and has no handler of theirs to change. That path is
`allow_raw_access: false`; the `http_request` check belongs to custom HTTP
canisters.
Also: the tree swap now restores the previous tree if the second rename fails,
rather than leaving the target absent; and three sentences used the bare
`static-site` where the naming rule wants either the recipe (`@dfinity/static-site`)
or the task ("a static site").
Raised rather than decided: the review asks for upstream license text and a
NOTICE entry for the synced tree. certified-assets is DFINITY Apache-2.0, the
same license and holder as this repo, and neither existing synced tree is in
NOTICE, including `docs/languages/motoko/`, which comes from another org. That
is a repo-wide question rather than something to settle for one tree in this PR.
Refs #396
|
Fifth Copilot review: one posted comment, answered in its thread, and ten suppressed. Six were ours, fixed in 1343a13. Three are upstream prose in the generated tree. One is a policy question worth a maintainer's decision rather than a fix here. Fixed, and two of these were producing wrong instructions
Extended beyond what was flagged The posted finding about the missing Upstream, not ours to change The two elided Raised rather than decided: license and NOTICE The suggestion is to carry the upstream license alongside the generated files and add a
So either the policy means third-party in the ownership sense and none of the three needs an entry, or all three do and that is a small separate change. Adding one only for certified-assets would leave the repo in the least coherent of the three states. |
Summary
Publishes the eight certified-assets user-doc pages as a synced tree under Frontends, and makes a static site the default path our own pages teach. They taught
@dfinity/asset-canisterwhile the templates, the icp-cli docs site, and this site all deploy@dfinity/static-site, and every configuration detail differs between the two.Closes #396
Decisions worth your judgment
.sources/upstream.jsonunder a newsyncedgroup andscripts/sync-static-site.mjsfetches the pages at it..github/workflows/sync-static-site.ymlopens a bump PR weekly for any release the pin does not contain: normally a content diff, and when a release touched no page, a pin-only PR whose whole diff issource_ref, which is what moves the pin off a commit and onto a tag.v0.3.3tag. Every commit past that release is docs-only, and the tag predates the frontmatter contract the sync needs, so the tag cannot be synced at all..sources/VERSIONSforbids pinning past a release; the reasoning for why this is not that case is in the entry's$comment, and the pin returns to tags when a release contains the pages.docs/languages/motokois. The sync normalizes and rewrites so the pages pass the same checks as a hand-written page, and exits non-zero instead of publishing pages that do not.validate.jsnow resolves link fragments repo-wide, because our pages deep-link headings upstream can rename. Seven links were already broken, three across pages and four within one. Anchors usegithub-slugger, the library Starlight itself uses, because a local approximation disagreed on 21 of the repo's 2995 headings. It was already present as a transitive dependency and is now declared, withpackage-lock.jsonhand-edited to a one-line diff (npm installon macOS prunes this lockfile).dfx, which this repo bans outright, so the sync translates that exact command to its verifiedicpequivalent, reports it, and refuses to publish any otherdfxshape rather than guessing. Everything else upstream writes is published as written..didat the pinned ref: no proposal-gated commit, no staged-batch evidence, authorization is controllers plus a flat syncer set).asset-canister.mdgains "When to stay on this canister" and the governance guide says which canister its workflow requires.Reviewing it
The commits are scoped so they can be read in order.
docs/guides/frontends/static-site/is generated: review the diff, not the pages, and never hand-edit them. The rest is our own prose, with the naming pass acrossconcepts/,getting-started/and the guides being mechanical.Five Copilot rounds have already swept the mechanical layer, and every finding is answered in the threads and in the
feedback-addressedcomments. What none of them reached, and what is worth your judgment: whether thedfxrewrite should exist at all, whether the naming pass belongs in this PR or its own, whether the Frontends structure reads right to a developer arriving cold, and whether the SNS exception is the right thing to tell people.Verified
npm run buildandnpm run validate --allpass on all 218 pages, including the synced tree; build warnings 276 → 275llms.txtderives both sectionsdocs/reference/cli.mdatv1.5.0; template recipes againstdfinity/icp-cli-templates; the certified-assets interface against its.didat the pinned refOpen question
Whether a synced tree needs the upstream license alongside it and an entry in
NOTICE.dfinity/certified-assetsis Apache-2.0, the same license and holder as this repo, and neither existing synced tree has an entry, includingdocs/languages/motoko/, which comes from another org. So either none of the three needs one or all three do; adding one only here would be the least coherent option.Out of scope
#279 (stale
<!-- Upstream: -->comments) is unblocked now that #355 is closed, but it is a sweep over 108 unrelated files; this sync writes provenance as frontmatter and adds no new instance. No upstream issue was filed for proposal-gated updates: the topic is already under consideration there, and the docs drop the exception if it ships.