Best for
- Use when asked to "deploy", "set up deployment", "configure adapter", "deploy to cloudflare", "deploy to netlify", "deploy to vercel", "static export", or "production build".
JoviDeCroock/pracht/skills/pracht-deploy/SKILL.md
Pracht deployment guide. Walks through adapter configuration, building, and deploying to Node.js, Cloudflare Workers, Netlify, Vercel, or a pure static host. Handles platform config, Docker and production checklist. Use when asked to "deploy", "set up deployment", "configure adapter", "deploy to cloudflare", "deploy to netlify", "deploy to vercel", "static export", or "production build".
Decision brief
Guided adapter setup and deployment for pracht applications.
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/JoviDeCroock/pracht --skill "skills/pracht-deploy"Inspect the Agent Skill "pracht-deploy" from https://github.com/JoviDeCroock/pracht/blob/21b95a63f0ee9ef30d4de1a2c512c317a10074df/skills/pracht-deploy/SKILL.md at commit 21b95a63f0ee9ef30d4de1a2c512c317a10074df. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
Read vite.config.ts and package.json first — don't assume the current adapter. Ask the user where they want to deploy if not already clear from their message.
1. Ensure @pracht/adapter-node is installed. 2. In vite.config.ts:
Review the “Supported Adapters” section in the pinned source before continuing.
1. Ensure @pracht/adapter-node is installed. 2. In vite.config.ts:
dist/client/ — static assets (JS, CSS, prerendered HTML)
Permission review
The documentation includes network, browsing, or remote request actions.
adapter: nodeAdapter({ canonicalOrigin: "https://app.example.com" }),The documentation includes network, browsing, or remote request actions.
Pin `canonicalOrigin` in production so `request.url` does not depend on theThe documentation asks the agent to run terminal commands or scripts.
node dist/server/server.jsThe documentation asks the agent to run terminal commands or scripts.
npx wrangler deployEvidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 93/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 93 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Guided adapter setup and deployment for pracht applications.
Read vite.config.ts and package.json first — don't assume the current adapter.
Ask the user where they want to deploy if not already clear from their message.
If the pracht MCP server is registered (docs/MCP.md), prefer the inspect_build/doctor/verify MCP tools over shelling out. Note: inspect_build (like pracht inspect build) needs a prior pracht build, and pracht inspect requires the pracht plugin registered in the vite config.
| Adapter | Package | Status |
|---|---|---|
| Node.js | @pracht/adapter-node | Stable |
| Cloudflare Workers | @pracht/adapter-cloudflare | Stable |
| Netlify | @pracht/adapter-netlify | Stable |
| Vercel | @pracht/adapter-vercel | Stable |
| Static export | @pracht/adapter-static | Stable |
@pracht/adapter-node is installed.vite.config.ts:
import { pracht } from "@pracht/vite-plugin";
import { nodeAdapter } from "@pracht/adapter-node";
export default {
plugins: [
pracht({
adapter: nodeAdapter({ canonicalOrigin: "https://app.example.com" }),
}),
],
};
Pin canonicalOrigin in production so request.url does not depend on the
incoming Host header. maxBodySize is also available on nodeAdapter().
Only custom entries behind a trusted proxy that overwrites forwarded headers
should use createNodeRequestHandler({ trustProxy: true }).
If that proxy strips Vite's deploy base from the forwarded path, set
nodeAdapter({ basePathStripped: true }) (or the same option on a custom
createNodeRequestHandler). Do not infer this from the first path segment: a
route may legitimately begin with the same segment as the deploy base. The
adapter restores the public base before createContext(), loaders, and API
handlers receive the request.
The proxy must also own the public bare-base redirect (/app to /app/) in
this mode because the stripped origin cannot distinguish it from a legitimate
base-free /app route.
The Node adapter compresses responses by default (brotli/gzip negotiated via
Accept-Encoding, streaming for dynamic bodies, an in-memory LRU for static
assets). When the deployment sits behind a reverse proxy or CDN that already
compresses responses, set nodeAdapter({ compression: false }) so bodies are
not compressed twice.
pracht build
Produces:
dist/client/ — static assets (JS, CSS, prerendered HTML)dist/server/server.js — Node server entrydist/server/isg-manifest.json — ISG revalidation config (if ISG routes exist)dist/client/.vite/manifest.json — asset manifest for script/style injectionnode dist/server/server.js
Port 3000 by default. For a local production smoke test, pracht preview builds and runs the server in one step (--port <n>, --skip-build to reuse an existing build). For production: reverse proxy (nginx, Caddy), process manager (PM2, systemd), NODE_ENV=production.
FROM node:22-alpine
WORKDIR /app
COPY dist/ dist/
COPY package.json .
EXPOSE 3000
CMD ["node", "dist/server/server.js"]
@pracht/adapter-cloudflare is installed.vite.config.ts:
import { pracht } from "@pracht/vite-plugin";
import { cloudflareAdapter } from "@pracht/adapter-cloudflare";
export default { plugins: [pracht({ adapter: cloudflareAdapter() })] };
pracht build
npx wrangler deploy
To smoke-test the built worker locally first, run pracht preview — it builds and then delegates to wrangler dev, which serves the wrangler config's main entry, dist/server/worker.js.
Wrangler owns the Worker's binding environment. Put local-only secrets such as
PRACHT_CONFIRMATION_SECRET and PRACHT_REVALIDATE_TOKEN in a gitignored
.dev.vars; prefixing the host command with those variables does not
automatically expose them inside the Worker. Keep production values in
wrangler secret.
If the config contains a custom-domain route, preview can listen on localhost
while request.url inside the Worker uses the custom domain. Sign that
effective @authority for Web Bot Auth or temporarily disable the route. To use
a separate local config, build first, then run:
pracht build
npx wrangler dev --config wrangler.local.jsonc --port 3000
The local config must keep main: "dist/server/worker.js" and omit the
production route. pracht preview does not forward Wrangler's --config flag.
// wrangler.jsonc
{
"name": "my-pracht-app",
"main": "dist/server/worker.js",
"compatibility_date": "2026-04-06",
"assets": {
"binding": "ASSETS",
"directory": "dist/client",
"run_worker_first": true,
},
}
"binding": "ASSETS" and "run_worker_first": true are required. Without the binding, the worker's env.ASSETS resolves to nothing and the runtime silently falls back to null — headers and ISG manifests load empty, so SSG serving, ISG revalidation, and per-route headers all silently no-op. The canonical config lives at examples/cloudflare/wrangler.jsonc. If you rename the binding with assetsBinding (below), the wrangler binding value must match.
export async function loader({ context }: LoaderArgs) {
const value = await context.env.MY_KV.get("key");
return { value };
}
Keep Cloudflare binding reads inside the loader, API handler, capability
run(), or another request-time function. Although Workers permits top-level
env.MY_KV, Pracht graph inspection intentionally fails such module-initializer
reads because it cannot supply an authoritative binding without risking false
graph metadata.
pracht({ adapter: cloudflareAdapter({ assetsBinding: "STATIC" }) });
Durable Object and Workflow classes are named Worker exports. Re-export them
from the module configured with workerExportsFrom. Queue consumers, Cron
Triggers, and Email Routing are instead methods on the default export; expose
named queue, scheduled, or email functions from the module configured
with workerHandlersFrom:
cloudflareAdapter({
workerExportsFrom: "/src/cloudflare.ts",
workerHandlersFrom: "/src/worker-handlers.ts",
});
ISG works out of the box: without any cache option, the default worker-managed path serves the build-time snapshot, detects staleness, and regenerates pages in the background via the Workers Cache API — per colo — and POST /__pracht/revalidate triggers on-demand regeneration. Enabling cache: true moves ISG from that per-colo worker-managed path to edge-tier Workers Caching, on both sides:
pracht({ adapter: cloudflareAdapter({ cache: true }) });
// wrangler.jsonc
{ "cache": { "enabled": true } }
Before enabling it, audit ISG URLs for unbounded query strings. Workers Caching
keys the exact path and query string, including parameter order and trailing
slashes; use a bounded query allowlist/canonical redirect or an uncached gateway
with a pathname-only cf.cacheKey, and normalize Accept there for routes that
export markdown or declare markdown: true for middleware-owned negotiation.
See docs/ADAPTERS.md#cache-key-cardinality.
Time-revalidated ISG pages then render on demand, are cached at the edge for
their revalidate window (stale pages served instantly while the Worker
re-renders in the background), and can be purged early with purgeCache() from
@pracht/adapter-cloudflare/cache. Webhook-only ISG routes keep their
build-time snapshots and the worker-managed path either way.
@pracht/adapter-netlify and netlify-cli are installed.vite.config.ts:
import { pracht } from "@pracht/vite-plugin";
import { netlifyAdapter } from "@pracht/adapter-netlify";
export default { plugins: [pracht({ adapter: netlifyAdapter() })] };
netlify.toml:
[build]
command = "pnpm build"
publish = "dist/client"
[functions]
directory = "netlify/functions"
npx pracht build && npx netlify dev
npx netlify deploy --build --prod
The build emits netlify/functions/pracht.mjs. Page requests go through that
function so Markdown negotiation and route-state requests remain correct;
hashed assets bypass it and stay outside the function bundle at the origin
root. With a Vite deploy base, the function instead bundles and serves the
base-free asset and /_pracht trees so /app/... requests remain inside the
mount. Custom excludedPath entries still bypass their literal origin-root
URLs, but matching files remain bundled for base-prefixed requests. The
generated config enumerates only client files the function can serve and roots
applicable exclusions at the function file so Netlify's tracer cannot re-add
bypassed trees. Netlify durable caching
implements time-based ISG and per-path cache tags implement authenticated
webhook revalidation. A trailing-slash ISG document request permanently
redirects to the canonical slashless URL before rendering, and webhook
revalidation normalizes either spelling before purging the cache tag.
Only Cache-Control, CDN-Cache-Control, and Netlify-CDN-Cache-Control
override the adapter's cache defaults; provider-specific headers for another
CDN do not. Set a cache window to 0 to disable stale serving or freshness.
Netlify-Vary owns route-state variants, while the standard Vary: Accept
header owns Markdown negotiation. Cacheable negotiated SSG representations use
the same Netlify-Vary instructions as their prerendered HTML. Shared ISG
renders strip visitor-specific request data and Netlify context metadata before
loaders or context factories run.
pracht preview exits with guidance because it cannot emulate Netlify's
Functions and CDN behavior. Build the generated function before using
netlify dev for the platform-shaped local runtime. Configure
PRACHT_REVALIDATE_TOKEN in Netlify when webhook revalidation is enabled.
@pracht/adapter-vercel is installed.vite.config.ts:
import { pracht } from "@pracht/vite-plugin";
import { vercelAdapter } from "@pracht/adapter-vercel";
export default { plugins: [pracht({ adapter: vercelAdapter() })] };
pracht build
npx vercel deploy --prebuilt
Produces: .vercel/output/config.json, .vercel/output/static/, .vercel/output/functions/render.func/server.js
There is no faithful local Vercel production runtime, so pracht preview
exits with guidance. Use vercel build or vercel dev. Set
PRACHT_REVALIDATE_TOKEN at build time when using webhook revalidation; its
Vercel bypass token is embedded in .prerender-config.json. Rename the main
Edge Function with vercelAdapter({ functionName }) if its default render
name would collide with an ISG route. Custom entries must export the
nodeListener created by createVercelNodeListener(handle) for Node ISR
functions.
For apps where every route is render: "ssg" (or loaderless, full-hydration
"spa"), with no
request middleware, API routes, or HTTP/MCP/WebMCP-exposed capabilities. SSG
loaders run only at build time and must produce HTML plus valid JSON route
state; dynamic SSG routes must export getStaticPaths(). Anything else fails the build with an error naming the
offenders — that is the signal to pick a serverful adapter instead. Only
manifest-registered capabilities participate; every registered capability
module must load successfully so exposure validation can fail closed. The
notFound page must use full hydration (the default), because the shared
404.html needs the client router to adopt the visitor's actual URL. Sub-path
deploys (GitHub Pages project sites, S3 key prefixes) set Vite base to that
path; CDN and document-relative bases ("" / "./") are build errors,
because they split assets from the deploy root or resolve them beneath nested
page directories. Under a base,
internal navigation must go through <Link route> / href() — a hand-written
<a href="/about"> still means the origin root.
Pracht's preview and first-party serverful adapters redirect the bare base
(/app) to its trailing-slash form (/app/) before serving the root document;
custom adapters receive the same behavior through handlePrachtRequest().
Framework-owned browser URLs from the default image loader and OpenAPI
companion artifacts pick up the same base automatically.
@pracht/adapter-static is installed.vite.config.ts:
import { pracht } from "@pracht/vite-plugin";
import { staticAdapter } from "@pracht/adapter-static";
export default { plugins: [pracht({ adapter: staticAdapter() })] };
// With dynamic SPA routes, add { fallback: "200.html" } and configure the
// host to rewrite unmatched URLs to it. If the route or shell exports
// head(), also set generic fallbackHead metadata shared by every rewrite.
pracht build # dist/client/ is the whole deployment
pracht preview # local static file server over dist/client/
Upload dist/client/ to any static host (GitHub Pages, S3, nginx, Netlify).
dist/server/ is build tooling only — never deploy it. The host must serve
<dir>/index.html for clean URLs and should use 404.html as its error
document. A static notFound page must use full hydration so that shared
document can adopt the visitor's real URL. Client navigation fetches collision-safe
bounded opaque .json files under _pracht/state/ for full-hydration SSG
routes whose loader or route/shell head() metadata participates in navigation;
equivalent raw-Unicode and percent-encoded URL segment spellings resolve to the
same state file. Explicitly loaderless and headless routes fetch no Pracht
state; loaderless routes with head metadata fetch static state for font-head
fragments but still use browser-side requests to an external API for live
data. Files under public/_pracht/state/ may not occupy a generated
route-state path; the build rejects the collision instead of overwriting the
public file. Files copied from public/ or emitted by Vite also may not occupy
the generated 404.html or configured fallback path, including a case- or
Unicode-normalization-equivalent spelling; the build rejects the portable
collision instead of overwriting existing output. Generic fallbackHead fonts
remain registered while the fallback commits a loaderless dynamic SPA route.
See docs/ADAPTERS.md § Static Adapter for host header
configuration and limitations (markdown negotiation, base paths). Pages are
written to the percent-decoded output path, matching how static hosts resolve
requests; pracht preview decodes request segments the same way. The SPA fallback only client-renders matched SPA routes; dynamic
SSG paths omitted by getStaticPaths() render the app's not-found page with
the build-time loader data or handled error state carried over from 404.html.
The host rewrite that serves the fallback answers unknown URLs with status 200 (soft 404), and an app
with no notFound page and no unshadowed client-routable SPA catch-all renders them blank — the build
warns about that shape. A dynamic SPA route, its shell, or the not-found page
with head() requires an explicit fallbackHead, because the shared static
document cannot evaluate URL-specific server metadata. Prerendered pages must
map to distinct portable filesystem paths; duplicate/case-folded or
Unicode-normalization-equivalent outputs, Windows-invalid or overlong filename
components, and file/directory conflicts such as / with /index.html fail
before any page is written. Fallback names likewise reject Windows reserved
device names and the portable 255-byte/code-unit component limit.
pracht build and verify dist/ output.dist/client/ contains prerendered HTML for SSG routes (and ISG routes — except time-revalidated ISG routes on Cloudflare with Workers Caching enabled, which render on demand; webhook-only ISG routes keep their build-time snapshots).dist/server/isg-manifest.json; on Cloudflare also dist/client/_pracht/isg.json) exists if using incremental static generation.pracht preview (or node dist/server/server.js).vite.config.ts and package.json before giving advice.pracht build to verify the build succeeds before deploying.pracht preview; for Netlify, run pracht build && netlify dev.pnpm add @pracht/adapter-*).$ARGUMENTS
Frequently asked questions
Guided adapter setup and deployment for pracht applications.
The source record exposes this install command: npx skills add https://github.com/JoviDeCroock/pracht --skill "skills/pracht-deploy". Inspect the command and pinned source before running it.
Static rules flagged network, exec-script in the source; the page lists the matching lines and excerpts.
Alternatives
Postpartum-genushyacinthus29/dotnet-skills
Build long-running .NET background services with `BackgroundService`, Generic Host, graceful shutdown, configuration, logging, and deployment patterns suited to workers and daemons.
vasilyu1983/AI-Agents-public
Guides iOS testing with XCTest, XCUITest, Swift Testing, simctl, and xcresult. Use when choosing destinations, controlling flakes, or parsing test artifacts for native apps.
garrytan/gbrain
Generate a publication-quality PDF from any brain page via the gstack make-pdf binary. Strips YAML frontmatter, sanitizes emoji, applies running headers and page numbers. Brain page is always the source of truth; PDF is a rendering.
NVIDIA/skills
How to swap the DeepStream CV detection model in the VSS Alerts Blueprint verification (2d_cv) mode - covers ONNX export, custom bbox parsers, compose mount gotchas, nvinfer config, runtime TRT engine build, deployment, and a segmentation-capable model addendum handoff.