RazorWire CLI
Source of truth
The RazorWire CLI is a command-line tool for managing RazorWire projects. Its primary feature is exporting a reactive RazorWire site into CDN-ready static files by default, with an opt-in hybrid mode for deployments that still provide server routing.
The CLI uses AppSurface's command-first console mode. That means help and validation output are intentionally quiet, without Generic Host lifecycle banners, while real export runs still emit useful progress logs.
Start here: export the sample
During repository development, the fastest confidence check is to export the RazorWire MVC sample from source:
dotnet run --project Web/ForgeTrust.RazorWire.Cli -- export -o ./dist -p ./examples/razorwire-mvc/RazorWireWebExample.csproj
test -f ./dist/index.html
A successful run publishes the sample, starts it on an ephemeral loopback URL, crawls the discovered pages, writes static files under ./dist, and shuts the target app down automatically. Inspect ./dist/index.html first; it proves the exporter emitted the root artifact.
Installation
The RazorWire CLI project is configured as a .NET tool with the command name
razorwire. Use an exact package version when running release builds so exports
are reproducible. The package chooser still excludes ForgeTrust.RazorWire.Cli
from the direct-install matrix until issue #171 lands stable public tool
packaging, so the commands in this section require the package to exist on one
of your configured NuGet sources or for you to pass an explicit local package
source. During normal repository development, prefer the source-run command below.
Run a published package without permanently installing it:
dnx ForgeTrust.RazorWire.Cli@<version> --yes -- export -o ./dist -p ./examples/razorwire-mvc/RazorWireWebExample.csproj
The equivalent SDK spelling is:
dotnet tool execute ForgeTrust.RazorWire.Cli@<version> --yes -- export -o ./dist -p ./examples/razorwire-mvc/RazorWireWebExample.csproj
Install the tool when you want a stable razorwire command on your PATH:
dotnet tool install --global ForgeTrust.RazorWire.Cli --version <version>
razorwire export -o ./dist -p ./examples/razorwire-mvc/RazorWireWebExample.csproj
During repository development, run the CLI directly from source:
dotnet run --project Web/ForgeTrust.RazorWire.Cli -- [command] [options]
When testing an unpublished package from a local folder, pack it first, pass that folder as the package source, and keep the version exact:
dotnet pack Web/ForgeTrust.RazorWire.Cli -c Release -o ./artifacts/packages /p:PackageVersion=0.0.0-local.1dnx ForgeTrust.RazorWire.Cli@0.0.0-local.1 --yes --source ./artifacts/packages -- --help
Do not combine --version and --prerelease for exact tool installs on recent SDKs; exact prerelease versions install without the extra flag.
Commands
Help and validation behavior
- Root help (
--help) and command help (export --help) are command-first by design. - Validation failures, such as invalid flags or missing source options, should surface actionable CLI output without host startup and shutdown chatter.
- Source validation failures include a concrete recovery path. When no source or multiple sources are provided, the error points to a single-source example such as
razorwire export --project ./MyApp.csproj --output ./distand reminds developers to runrazorwire export --help. - Successful export runs still keep command-owned progress output so long-running work remains understandable.
export
Exports a RazorWire application to a static directory.
Options:
-o|--output <path>: Output directory where the static files will be saved (default:dist).-r|--seeds <path>: Optional path to a file containing seed routes to crawl.--publish-root-extras <path>: Optional YAML manifest for explicit single-file deployment extras copied into the publish root after generated output is proven collision-free.-u|--url <url>: Base URL of a running application used for crawling.-p|--project <path.csproj>: Path to a .NET project to run automatically and export.-d|--dll <path.dll>: Path to a .NET DLL to run automatically and export.-f|--framework <TFM>: Target framework for project exports. Required when--projectpoints at a multi-targeted project.-m|--mode <cdn|hybrid>: Export mode.cdnis the default and rewrites managed internal URLs to emitted static artifacts.hybridpreserves application-style internal URLs for hosts that still route extensionless paths.--live-origin <origin>: Optional live origin for split-origin hybrid exports, such ashttps://api.example.com. The value must be anhttporhttpsorigin only, with no path, query, fragment, or userinfo.--hybrid-credentials <auto|include|omit>: Credential behavior for RazorWire-managed live calls.autois the default and includes credentials when--live-originis set; useomitonly for intentionally anonymous live endpoints.--app-args <token>: Repeatable app-argument token to pass through when launching--projector--dll.--no-build: Project mode only. Skips the release publish step and reuses existing published output.
Exactly one source option is required: --url, --project, or --dll.
Export modes
cdn mode is the default because most razorwire export runs produce folders that are uploaded to plain static hosting or a CDN. It emits extension-backed artifact URLs for exporter-managed internal references:
/is emitted asindex.htmland may still be referenced as/./aboutis emitted asabout.htmland internal references rewrite to/about.html./docs/startis emitted asdocs/start.htmland internal references rewrite to/docs/start.html.- Dotted page slugs still follow page-route rules:
/docs/web/forgetrust.razorwireis emitted asdocs/web/forgetrust.razorwire.html, while assets that return non-HTML content keep their real extension. - AppSurface Docs content frames also emit
.partial.htmlartifacts when adoc-contentframe exists, so static frame navigation can fetch the content island. - Host-registered seed routes are crawled in addition to configured seed files or in-memory defaults. This lets a host with its own route graph export public pages that are not linked from the initial crawl roots.
- Redirect aliases registered by the host are validated after their canonical routes are proven. The default
htmlredirect strategy emits tiny alias HTML fallback files after canonical artifacts; provider integrations can instead selectnetlifyto write exact root_redirectsrules and skip alias HTML files. If a host registers aliases, it should also register or otherwise expose their canonical routes as crawl seeds so validation can prove the canonical artifacts exist. - Assets that already have extensions, such as
/css/site.css,/img/logo.png, or/_content/.../razorwire.js, keep their path. Cache-busting query strings on assets are allowed only when the query-free path maps to an exported file. - The conventional
/_appsurface/errors/404page, when available, is emitted as404.htmland participates in the same CDN validation and URL rewriting. - Exporter-managed artifact fetches own their HTTP redirect chain before reading response bodies. Same-scheme, same-host, same-port redirects are allowed only when the final response stays under the configured
--urlor launched app base path; redirects outside that export origin and app path fail withRWEXPORT008. This is separate from host-registered redirect aliases (RWEXPORT005) and from URL-source readiness probing, which only checks whether a running app responds.
Generated export artifact boundary
Generated artifacts are written only inside the physical output tree. Before creating artifact parents, opening final files, enumerating release archive entries, reading file lengths, or hashing bytes, export checks the output root, existing parent segments, and existing targets for symlinks, junctions, or other reparse points. This guard covers HTML, CSS, binary assets, root 404.html, AppSurface Docs .partial.html files, redirect alias HTML, Netlify _redirects, .appsurface-docs-route-manifest.json, and .appsurface-docs-release-manifest.json.
Failures use RWEXPORT009 with stable reason labels: output-root-reparse, artifact-parent-reparse, artifact-target-reparse, archive-entry-reparse, and artifact-outside-root. The diagnostic includes the problem, cause, fix, artifact kind, route when available, output-relative path, unsafe segment, operation (create-directory, open-write, archive-enumerate, or archive-hash), and this docs anchor. It deliberately reports the unsafe output segment rather than resolving or printing the external symlink target.
This is distinct from RWEXPORT008: RWEXPORT008 protects artifact HTTP redirect provenance before response bodies are read, while RWEXPORT009 protects the local filesystem boundary before generated bytes are written or release archive bytes are read. Hostile concurrent mutation after validation is outside the exporter contract; run export into an operator-owned build directory that other processes do not modify.
Use this quick chooser before exporting:
| Goal | Mode |
|---|---|
| Upload one self-contained folder to GitHub Pages, Netlify, S3, or a plain CDN | cdn |
| Serve static pages behind app-aware routing or live RazorWire frames, forms, streams, and islands | hybrid |
| Make a missing CSS, image, script, stylesheet, module preload, icon, font, or other browser asset stop failing | Fix or externalize the asset; hybrid still validates browser-delivered assets |
Static website deployment extras
RazorWire export owns the app route graph and exporter-managed provider artifacts. It does not copy arbitrary project-root files into the output directory. Use this boundary when preparing folders for static hosts:
--seedsare routes, not local paths. A seed file line such as/pricingasks the exporter to crawl the app route/pricing; a line such asdeploy/CNAMEis not a request to copy./deploy/CNAME.--publish-root-extrasare deployment-owned publish-root files, not routes. The option reads a checked-in YAML manifest and copies only the named regular files after export proves each target path is not generated, reserved, preexisting, or hidden inside an exact release archive.- The conventional AppSurface 404 page uses the reserved
/_appsurface/errors/404route. When that route is available and returns HTML, the exporter stages it as root404.htmlbefore normal crawl processing and validates it like any other CDN artifact. CNAME,.nojekyll, and/.well-known/security.txtare deployment-owned extras when your host needs them. Keep them in a deployment directory such asdeploy/and name each file explicitly indeploy/export-extras.yml./_redirectsand/_headersare reserved in v1._redirectsis exporter-owned when the export strategy is Netlify, for example through a host integration usingExportRedirectStrategy.Netlifyor throughappsurface docs export --redirects netlify. Future provider headers and redirects should be structured exporter features, not raw copy-through files.
The manifest schema is intentionally small:
version: 1
extras:
- source: CNAME
publishPath: /CNAME
source resolves relative to the manifest directory and must stay under that directory without symlink or reparse traversal. publishPath is a publish-root file path, not a route: it must start with /, must not contain traversal, encoded slashes, query strings, fragments, protocols, control characters, or invalid filesystem segments, and is matched case-insensitively against exporter-owned paths.
For GitHub Pages branch or folder publishing with a custom domain:
mkdir -p deploy
printf '%s\n' 'www.example.com' > deploy/CNAME
cat > deploy/export-extras.yml <<'YAML'
version: 1
extras:
- source: CNAME
publishPath: /CNAME
YAML
razorwire export --project ./MySite.csproj --output ./dist --publish-root-extras ./deploy/export-extras.yml
test -f ./dist/CNAME
When the site also needs an app-served /404.html route, add /404.html to --seeds only when the app intentionally serves that route and is not relying on the conventional AppSurface 404 route. When the app uses AppSurface Web's conventional browser status pages, let /_appsurface/errors/404 stage root 404.html; adding /404.html to the seed file will not override the already staged conventional page.
Custom GitHub Actions Pages artifact workflows can still assemble the final upload around the exported folder. Keep the extras manifest in the repository, run razorwire export with --publish-root-extras, then upload the resulting publish root; do not add a later broad copy step that can overwrite exporter-owned files.
| Need | Recommended path |
|---|---|
| Conventional AppSurface 404 page | Serve /_appsurface/errors/404; export stages root 404.html when the route returns HTML. |
Host-served custom /404.html route without conventional AppSurface 404 |
Add /404.html to the seed route file. |
| GitHub Pages custom domain | Add deploy/CNAME to deploy/export-extras.yml with publishPath: /CNAME, then export with --publish-root-extras. |
| GitHub Pages Jekyll bypass | Add deploy/.nojekyll with publishPath: /.nojekyll. |
| Security contact file | Add deploy/.well-known/security.txt with publishPath: /.well-known/security.txt. |
| Netlify redirect aliases | Use ExportRedirectStrategy.Netlify in the host integration, or appsurface docs export --redirects netlify for AppSurface Docs, so the exporter owns _redirects. |
| Netlify headers | Reserved in v1; do not copy /_headers through deployment extras. |
| AppSurface Docs exact release archive plus deployment extras | Keep exact release trees clean for .appsurface-docs-release-manifest.json; copy publish-root extras such as CNAME into the surrounding publish root outside the immutable exact release tree. |
Why not public/? RazorWire exports from the running app and validates the generated artifact graph before writing deployment extras. A framework-style public/ overlay would blur route-owned files, provider-owned files, stale output, secrets, and generated artifacts. Use deploy/export-extras.yml instead: every extra is named, validated, copied without overwrite, and rejected if it collides with a generated route artifact, provider file, 404.html, docs archive manifest, or existing final target.
Migration examples:
| Old step | New step |
|---|---|
install -m 0644 ./deploy/CNAME ./dist/CNAME |
Add source: CNAME, publishPath: /CNAME to deploy/export-extras.yml; pass --publish-root-extras ./deploy/export-extras.yml. |
cp -R public/* dist/ |
Move only needed single files into deploy/, list each one in the manifest, and let export reject collisions. |
printf '/old /new 301!' > dist/_redirects |
Register redirect aliases and select ExportRedirectStrategy.Netlify, or use appsurface docs export --redirects netlify for AppSurface Docs. |
Do not copy the repository root, broad globs, symlinks, local config, secrets, generated credentials, or unrelated deployment folders into ./dist. Deployment extras are explicit single regular files only; v1 has no directories, globs, symlink following, overlays, overwrite mode, provider profiles, or raw provider headers/redirects.
Troubleshooting:
| Symptom | Cause | Fix |
|---|---|---|
CNAME is missing from the uploaded site |
Export crawled app routes but no extras manifest registered the deployment-owned file. | Add deploy/export-extras.yml, pass --publish-root-extras, and verify test -f ./dist/CNAME in CI. |
A seed file contains deploy/CNAME, but the export still succeeds without CNAME |
Seeds are routes, not local paths; if no valid seed routes remain, export can fall back to /. |
Put route paths such as / and /404.html in the seed file, then register CNAME with --publish-root-extras. |
404.html is missing or not the page you expected |
The app neither served the conventional /_appsurface/errors/404 HTML route nor exposed the intended /404.html route as a seed. |
Use the conventional route for AppSurface 404 pages, or seed /404.html for a deliberate app-served page and verify test -f ./dist/404.html. |
RWEXPORT007 [target-reserved] reports /_redirects or /_headers |
The manifest tried to raw-copy a provider file reserved by the exporter. | Remove the raw extra and select the structured redirect/header feature when available; for Netlify redirects, use the Netlify redirect strategy. |
RWEXPORT007 [target-generated-collision] reports an extra path |
The manifest target matches a generated route artifact, redirect artifact, 404 page, provider file, docs manifest, or existing output file. | Change the deployment file path or remove the extra; generated output owns that publish path. |
RWEXPORT009 [artifact-parent-reparse], [artifact-target-reparse], or [archive-entry-reparse] reports a generated artifact |
The output tree contains a symlink, junction, or reparse point in a generated artifact path or release archive entry. | Export to a clean regular directory tree, remove the reparse point, or copy deployment-owned files into the surrounding publish root after export. |
| AppSurface Docs exact release verification fails after adding hidden deployment files | The .appsurface-docs-release-manifest.json archive contract expects a clean exact release tree. |
Export exact releases to a clean directory, pin the manifest, then copy deployment extras into the surrounding publish root. |
CDN validation fails the export when exporter-managed dependencies cannot be represented as static artifacts. Diagnostics use stable codes and include the discovered surface, such as <img src>, <a href>, stylesheet url(), or stylesheet @import string, plus the normalized path the exporter tried to prove:
RWEXPORT001: a server-fetched frame route did not materialize. Add or seed the frame route, or switch to--mode hybridwhen a live server owns it.RWEXPORT002: a query-bearing frame route cannot be represented as one static artifact. Export a query-free route, split the frame into static pages, or keep server routing with--mode hybrid.RWEXPORT003: a required internal asset did not materialize. Add the asset route, correct path casing, make the reference external/data/hash-only, or use--mode hybridonly when the missing reference is not a browser-delivered asset.RWEXPORT004: a managed internal URL could not be rewritten to an emitted artifact URL. Seed or expose the target route so it emits an artifact, or mark authoring-only anchors withdata-rw-export-ignore.RWEXPORT005: a registered redirect alias cannot safely point at its canonical route, collides with another route or provider redirect output, or was already crawled as a normal HTML page. Fix the host redirect registration so aliases map to exported canonical routes without artifact collisions.RWEXPORT006: an export found anti-forgery behavior that cannot run safely in the selected mode. CDN mode rejects any anti-forgery surface because a plain static host cannot mint runtime tokens. Hybrid RazorWire-managed forms are auto-converted to lazy token refresh when they have a safe app-owned action and credentialed split-origin live calls are enabled. Export fails early when the form opts out, posts to an external action, uses--hybrid-credentials omitwith--live-origin, or is not managed by RazorWire.RWEXPORT007: a publish-root deployment extra could not be accepted. Stable reason labels includeschema,source-outside-root,source-missing,source-directory,source-symlink,target-invalid,target-duplicate,target-reserved,target-generated-collision,target-exists,target-parent-symlink,copy-failed, andrelease-archive-incompatible.RWEXPORT008: an exporter-managed artifact fetch was redirected outside the configured export origin and app path, returned an invalid redirect, or exceeded the redirect limit. Keep export redirects on the same scheme, host, port, and base path; otherwise make the reference external instead of exporter-managed. The diagnostic names the original route and rejected target without reading or writing the redirected response body.RWEXPORT009: a generated artifact path or release archive entry would cross the physical output-root boundary through a symlink, junction, reparse point, or lexical escape. Stable reason labels includeoutput-root-reparse,artifact-parent-reparse,artifact-target-reparse,archive-entry-reparse, andartifact-outside-root.RWEXPORT010: static auth projection found protected allowed content, missing fallback content, auth diagnostics, unsafe auth metadata, or DevAuth/test persona markers in a generated text artifact. Stable reason labels includeauth-missing-fallback,auth-private-content,auth-unsafe-metadata,auth-diagnostics, andauth-artifact-leak. Add an explicitrw:auth-anonymousfallback, move protected UI behind live rendering, disable diagnostics/DevAuth for exported routes, or remove the route from static export. See Static Auth Projection.
Route normalization validates requested URLs before the exporter fetches them. Redirect enforcement validates the response chain before artifact content is read or written. The generated artifact boundary then validates the local output path before directory creation, final writes, archive enumeration, metadata reads, and hashing.
hybrid mode preserves the older application-style URL behavior. Use it when the exported directory will still be served by infrastructure that resolves extensionless URLs, dynamic frame endpoints, or other live-server behavior. Hybrid mode tolerates missing page and live-behavior references that server-backed infrastructure may own, but it still fails missing browser-delivered static assets with RWEXPORT003: scripts, stylesheets, module preloads, icons, images, srcset candidates, CSS url(...), CSS @import, and asset-shaped preload or prefetch hints. Hybrid export also fails unsafe artifact redirects with RWEXPORT008; same-origin redirects remain supported, but redirected artifact responses must stay inside the configured export origin and app path. Canonical metadata, DNS hints, anchors, and ambiguous page/API-shaped preload or prefetch hints are not hybrid static-asset failures. Safe RazorWire forms with static anti-forgery tokens are converted to lazy runtime refresh in hybrid mode even without --live-origin; this supports same-origin CDN passthrough to the backend for RazorWire endpoints.
Split-origin hybrid export is enabled by adding --live-origin to --mode hybrid:
razorwire export --mode hybrid \
--live-origin https://api.example.com \
--project ./MyApp.csproj
With a live origin, the exporter rewrites RazorWire-owned runtime surfaces to the live app: rw-stream-source URLs, server-backed island frame sources, safe RazorWire forms, and path-base-aware lazy anti-forgery endpoints. If a RazorWire form contains crawler-minted __RequestVerificationToken inputs, export removes those stale static token inputs and marks the form data-rw-antiforgery="lazy" so the browser fetches a fresh token from /_rw/antiforgery/token on first intent or immediately before submit. When the crawl target is mounted under a path base, the exporter strips that crawl path base from data-rw-antiforgery-endpoint before the runtime combines it with --live-origin; otherwise split-origin deployments would call the crawler path instead of the live app root. The runtime wakes the live origin only when the user interacts with the form, not on every static page visit.
For a local proof path, Cloud Run live-origin deployment recipe, CORS setup, cold-start tradeoffs, and AppSurface Docs export example, see Hybrid Hosting With Cloud Run.
The per-form rw-antiforgery="lazy" TagHelper attribute is optional. Use it as an assertion when authoring a form that must lazy-refresh, but split-origin export applies the safe conversion automatically. rw-antiforgery="off" is an explicit opt-out and will fail export if the form still contains a static anti-forgery token.
Redirect strategy is a host-integration setting, not a generic razorwire export CLI option. ExportContext.AddRedirectAlias(...) is the preferred API for registering alias-to-canonical relationships; AddRedirectArtifact(...) remains as a compatibility wrapper for older integrations. ExportRedirectStrategy.Html works on generic static hosts such as GitHub Pages. ExportRedirectStrategy.Netlify is for CDN exports published to Netlify-compatible providers: it writes one publish-root _redirects file, serializes exact site-local paths with per-segment percent encoding, uses 301!, de-duplicates exact serialized source/target pairs, rejects self-redirects after serialization, rejects same-source/different-target rules, rejects aliases that conflict with exported _redirects, and never uses PublicOrigin or emitted .html artifact URLs as rule targets.
CDN mode validates the static references the exporter owns and can see while crawling HTML and CSS: parser-discovered page links, Turbo Frame sources, supported HTML asset references, <link rel="canonical"> values that point at managed app routes, <img> and <source> srcset candidates, CSS url(...) references, and both @import url(...) and string-form @import "..." stylesheet dependencies. Hybrid mode runs the browser-asset part of that validation without requiring every page or live route to become a static artifact. It does not prove arbitrary app-authored JavaScript fetch calls, form posts, Server-Sent Events, import maps, or other runtime behavior that is not represented as exporter-managed markup or CSS references.
Parser-backed discovery can surface valid HTML or CSS references that older exporter versions missed. After upgrading, a new RWEXPORT### failure may be correct rather than a regression: export the route or asset, fix path casing, mark authoring-only anchors with data-rw-export-ignore, or choose --mode hybrid when the dependency is intentionally served by live infrastructure. Do not use data-rw-export-ignore for missing static assets.
For a missing asset such as background-image: url('/img/map-image.png'), fix the export by copying the asset into the crawled app, correcting path casing, changing the URL to an external asset, or removing the browser dependency. link rel="modulepreload" always validates as a static module asset. link rel="preload" and link rel="prefetch" fail only when the hint is clearly asset-shaped, such as as="script", as="style", as="image", as="font", or a URL ending in a known static extension. Page-shaped hints such as <link rel="prefetch" href="/docs/next"> remain live/page references in hybrid mode.
CDN export skips relative anchors that point at common source or project file extensions, such as ./Program.cs or ../Project.csproj, because those links are usually for GitHub and editor navigation rather than static-site dependencies. For other authoring-only anchors in app-rendered HTML, use data-rw-export-ignore; the anchor remains rendered and clickable, but CDN export will not crawl, validate, or rewrite its href.
When launched app processes are started by the CLI (--project or --dll), they run in production environment (DOTNET_ENVIRONMENT=Production, ASPNETCORE_ENVIRONMENT=Production).
When --project is used:
- Project mode publishes a release build by default.
- The publish probe disables persistent build servers so command output capture cannot be held open by reused MSBuild nodes.
- Multi-targeted projects must pass
-f|--framework <TFM>to select the target framework, for example-f net6.0or--framework net7.0; omitting it causes a CLI error before publish.-f|--frameworkcan be combined with--projectand--no-buildwhen reusing existing published output. - Project mode resolves the published app DLL and launches that DLL for crawling.
- Add
--no-buildto skip publishing and reuse existing published output.
When --dll is used:
- The CLI launches the provided DLL directly (no build or DLL resolution step).
For both --project and --dll:
- If you do not pass
--urlsvia--app-args, the CLI appends--urls http://127.0.0.1:0. - The launched app inherits the parent process environment, while the CLI forces
ASPNETCORE_ENVIRONMENT=ProductionandDOTNET_ENVIRONMENT=Productionfor deployed-runtime semantics. - The CLI waits for startup, crawls the app, then shuts the process down automatically.
- Pass multiple target-app arguments by repeating
--app-argsonce per token. For example,--app-args --urls --app-args http://127.0.0.1:5009launches the app with--urls http://127.0.0.1:5009.
If export fails
Process failures are reported with the command stage, exit code or startup exception when available, captured target-app stdout/stderr, and the next recovery step. Target-app output is fully captured for the current export attempt; the CLI does not apply a line-count or byte-count truncation limit. Common branches:
- Missing source option or multiple sources: choose exactly one of
--url,--project, or--dll; runrazorwire export --helpfor the current command shape. - Multi-targeted project without
--framework: pass-f|--framework <TFM>, such as--framework net10.0, so publish and DLL resolution use the same target. - Project publish fails: read the captured
dotnet publishstdout/stderr, fix the build error, and rerun the same export command. - Target app exits before listening: inspect the captured target-app output in the error. The app failed during startup before the exporter could discover a base URL.
- Readiness timeout after a listening URL: verify the app can serve requests at the emitted loopback URL and that startup work is not blocking the first response.
- Cancellation or interrupted export: the CLI performs best-effort shutdown of the launched target app before returning.
AppSurface Docs versioned export notes
When the target app hosts AppSurface Docs:
- Use
appsurface docs export --repo .for AppSurface's own repository docs surface. That command starts the AppSurface Docs standalone host in-process, uses the same packaged static-asset fallbacks as preview, derives default seeds from AppSurface Docs routing, and keeps-rreserved for--repo. - Export the live unreleased preview surface from its configured live docs root, such as
/docswhen versioning is off,/docs/nextwhen versioning is on with defaults, or/foo/bar/nextwhen the host setsAppSurfaceDocs:Routing:RouteRootPathto/foo/bar. - Export exact published release trees as standalone static subtrees that already contain their own
.appsurface-docs-route-manifest.json,.appsurface-docs-release-manifest.json,index.html,search.html,search-index.json,search.css,search-client.js,minisearch.min.js, section routes, and detail pages. - Keep exact-tree
search-index.jsondocument paths canonical and deployment-independent. Valid release search rows use root-relative/docs/...paths; they do not include request path bases, custom route roots, origins, executable schemes, traversal, or docs operational routes. AppSurface Docs rebases those canonical paths when the archive is mounted at{RouteRootPath},{RouteRootPath}/v/{version}, a custom route root, or a virtual directory. - The hidden
.appsurface-docs-route-manifest.jsonfile freezes canonical routes plus source-shaped and declared aliases from the release being exported. AppSurface Docs uses it when mounting exact archives so old aliases redirect to the archive-local canonical page instead of using whatever route rules current docs have later. - The hidden
.appsurface-docs-release-manifest.jsonfile records the final exported files and SHA-256 digests after materialization. Export logs the manifest SHA-256 and a copy-ready"releaseManifestSha256": "..."snippet for the AppSurface Docs version catalog. - Export fails with
ASDOCSARCHIVE005when the output directory contains unsupported hidden paths such as.nojekyllor.well-known/...; export to a clean directory before pinning the manifest digest. - Plain
razorwire exportremains generic and does not emit.appsurface-docs-release-manifest.json; useappsurface docs exportfor AppSurface Docs exact release archives that need a catalog pin. - Treat those exact release trees as immutable publish artifacts. The AppSurface Docs runtime mounts them later under
{RouteRootPath}/v/{version}and may also mount the recommended one at{RouteRootPath}. - The exporter recognizes custom-root AppSurface Docs pages by their AppSurface Docs client configuration or
doc-contentframe, not just by a/docsURL prefix. This keeps static partial generation working for mounted roots such as/foo/bar/next. - Use
--seedswhen you want deterministic seeds for docs-specific surfaces instead of relying only on crawl discovery. - For release publishing, set
AppSurfaceDocs__Harvest__FailOnFailure=truein the parent environment beforerazorwire export --projectorrazorwire export --dll. The launched target app inherits that setting, fails before listening when aggregate harvest health isFailed, and the export command surfaces the target app's startup output. The strict exception summary is redacted, but ordinary target host logs may still contain operator diagnostics such as repository paths or raw exception messages.
Keep razorwire export for arbitrary RazorWire apps where the CLI must launch a --project, launch a --dll, or crawl a pre-running --url. Use appsurface docs export when the AppSurface CLI owns the AppSurface Docs repository host and should avoid the generic child-process startup path.
Example:
dotnet run --project Web/ForgeTrust.RazorWire.Cli -- export -o ./dist -u http://localhost:5233dotnet run --project Web/ForgeTrust.RazorWire.Cli -- export -o ./dist -p ./examples/razorwire-mvc/RazorWireWebExample.csprojdotnet run --project Web/ForgeTrust.RazorWire.Cli -- export -o ./dist -d ./bin/Release/net10.0/MyApp.dll --app-args --urls --app-args http://127.0.0.1:5009Contributor process execution policy
RazorWire CLI owns two process boundaries:
| Boundary | Use for | Contract |
|---|---|---|
| Command executor | Finite commands such as publish probes and MSBuild property reads. | Return exit code, stdout, and stderr as data; do not throw for non-zero exits or ordinary startup failures; propagate cancellation. |
| Target app process | Long-running app launched for crawling. | Raise stdout/stderr as non-empty lines, surface startup failure before readiness timeout, raise exit after output drain when possible, and perform best-effort process-tree cleanup on disposal. |
Keep command arguments tokenized as ordered argument lists. Do not build shell command strings. User-facing docs should describe observable behavior and recovery steps; implementation dependencies belong in contributor docs and tests.