parkstatic-local-deploy

v2026.07.29

Deploy a static site to Parkstatic (WordPress) from a local machine or AI agent, without GitHub Actions. Detects the package manager and framework, builds, zips, and POSTs dist.zip to the Parkstatic deploy endpoint (https://deploy.parkstatic.site) using the Parkstatic deploy secret.

GitHub
安装命令
npx skhub add ParkStatic/parkstatic-local-deploy
Markdown
SKILL.md

Parkstatic local deploy (agent-driven, no GitHub Actions)

Use this skill to deploy a static site to Parkstatic directly from a local machine or an AI agent, bypassing the ParkStatic/action GitHub workflow. The canonical deploy path is the deploy.sh script in ParkStatic/action; this skill reproduces its three meaningful steps — build → package → upload — as plain local commands, so an agent (or a developer) can ship a build from a laptop, a CI runner without the action installed, or an ad-hoc prompt.

The deploy target is the official Parkstatic deploy endpoint at https://deploy.parkstatic.site. It authenticates the upload, parks the artifact in private storage, signs a short-lived download URL, and hands it off to the WordPress site's receiver. The receiver pulls the zip, extracts it, and serves it. With X-Parkstatic-Wait: true the endpoint waits for WordPress to confirm the install, so a WP-side failure surfaces as a non-2xx here instead of going green silently.

When to use

  • Deploying from a local clone with the Parkstatic deploy secret on hand.
  • An AI agent shipping a build on behalf of the user (no GitHub Actions run available).
  • Dry runs / hotfixes where spinning up a full Actions workflow is overkill.

Prerequisites

  1. A Parkstatic license and the deploy secret. The secret authorizes the upload; the agent cannot deploy without it. Obtain it by completing the official onboarding at ****:
    1. Buy a license and download the plugin from your account.
    2. Install + activate the plugin on your WordPress site (Plugins → Add New → Upload Plugin), enter your license key.
    3. The plugin's onboarding generates a unique PARKSTATIC_SECRET (Step 4 of the get-started walkthrough). Copy it from Parkstatic → General → Deploy secret in WP admin. The secret is sent to the deploy endpoint as X-Parkstatic-Token. Treat it as a credential — never commit it, never paste it into committed files, redact it from logs and transcripts.
  2. Deploy URL — the official Parkstatic deploy endpoint: https://deploy.parkstatic.site
  3. A project that builds to static HTML. Parkstatic supports Vite, Astro, SvelteKit, Remix, React Router, Nuxt, Next.js (static export), Lovable, TanStack Start, plain React/Vue SPAs, and more. The build must emit an index.html the action's locator would recognise (see step 5). For SSR-only builds that need a headless crawl to materialise HTML, use the GitHub Action instead — this skill deploys the build output verbatim and does not crawl.
  4. zip and curl on PATH, and git in the repo (git only populates the X-Parkstatic-* metadata headers; deploy does not require a push).

Execution spine

Run every step from the repository root. The skill is package-manager- and framework-agnostic: it detects the manager from the lockfile and runs the project's own build script, then locates the output with the same candidate-list heuristic the action uses.

1. Detect the package manager

Pick the manager from the lockfile present at the repo root (a simplified detection that covers the common cases; for exact parity with CI use the GitHub Action):

if [ -f pnpm-lock.yaml ]; then
  PM=pnpm
elif [ -f bun.lock ]; then
  PM=bun
elif [ -f bun.lockb ]; then
  PM=bun
elif [ -f yarn.lock ]; then
  PM=yarn
elif [ -f package-lock.json ]; then
  PM=npm
else
  PM=npm # fallback: npm with no lockfile
fi
echo "Package manager: $PM"

2. Use the right Node version

Match the version pinned in .nvmrc / .node-version if present (the action defaults to Node 22):

if [ -f .nvmrc ] || [ -f .node-version ]; then
  source ~/.nvm/nvm.sh 2>/dev/null && nvm use
fi
node --version

3. Install dependencies

Install with the detected manager. Use the frozen/ci install when a lockfile exists, falling back to a plain install if it's out of sync:

case "$PM" in
  pnpm) pnpm install --frozen-lockfile || pnpm install ;;
  npm)  npm ci || npm install ;;
  yarn) yarn install --frozen-lockfile || yarn install ;;
  bun)  bun install ;;
esac

4. Build

Run the project's own build script. Every framework Parkstatic supports exposes its build through the build script in package.json (vite build, astro build, nuxt build, next build, remix vite:build, react-router build, svelte-kit build, TanStack Start's vite build, Lovable's node scripts/generate-build-artifacts.mjs && vite build, etc.), so a single run build covers them all:

case "$PM" in
  pnpm) pnpm run build ;;
  npm)  npm run build ;;
  yarn) yarn build ;;
  bun)  bun run build ;;
esac

If package.json has no build script, fall back to vite build (the action does the same):

node -e 'const p=require("./package.json");process.exit(p.scripts&&p.scripts.build?0:1)' 2>/dev/null \
  || "$PM" exec vite build

Framework-specific notes (only when the project deviates from the standard build script):

FrameworkBuild commandTypical output dir
Vite (SPA)vite builddist
TanStack Start (prerender)vite build (with prerender: { enabled: true })dist/client (already-prerendered HTML)
TanStack Start (SSR, no prerender)vite builddist/server + dist/client — needs the action's crawl; do not deploy locally
Lovable (vite-tanstack-config)node scripts/generate-build-artifacts.mjs && vite builddist/client
Astro (static)astro builddist
Astro (SSR adapter)astro buildadapter-specific — needs the action's crawl; do not deploy locally
SvelteKit (static adapter)svelte-kit build (via build script)build
Remix (Vite)remix vite:build (via build script)build/client
React Router (framework)react-router build (via build script)build/client
Nuxt (static / nuxi generate)nuxt generate (via build script).output/public
Next.js (static export)next build (with output: 'export')out

5. Locate the static output directory

The action's find_output_dir heuristic checks, in order: dist/client, dist, build, build/client, .output/public, out — the first one containing an index.html wins. Replicate that locally (it already covers every framework in the table above):

OUTPUT_DIR=""
for candidate in dist/client dist build build/client .output/public out; do
  if [ -f "$candidate/index.html" ]; then
    OUTPUT_DIR="$candidate"
    break
  fi
done
[ -n "$OUTPUT_DIR" ] || { echo "No static output found (no index.html in any candidate dir)"; exit 1; }
echo "Output: $OUTPUT_DIR"

For TanStack Start prerendered builds, confirm the framework's own hydration bootstrap is present (the action keys off this marker and ships the build verbatim, skipping any crawl):

grep -qE '\$_TSR|\$tsr-stream-barrier' "$OUTPUT_DIR/index.html" \
  && echo "prerendered build — deploy verbatim" \
  || echo "static SPA build"

6. Package into dist.zip at the workspace root

Zip the contents of $OUTPUT_DIR (not the directory itself) into dist.zip at the repo root. Use an absolute path so the destination is correct regardless of how deep $OUTPUT_DIR is.

ZIP_PATH="$(pwd)/dist.zip"
rm -f "$ZIP_PATH"
( cd "$OUTPUT_DIR" && zip -qr "$ZIP_PATH" . )

7. Strip unsafe entries (WordPress zip validation)

WordPress's receiver rejects archives containing unsafe paths or file types with parkstatic_unsafe_zip_entry (wp_status: 400). The most common offender on macOS is .DS_Store; exclude all dotfiles to be safe:

rm -f "$ZIP_PATH"
( cd "$OUTPUT_DIR" && zip -qr "$ZIP_PATH" . -x '.DS_Store' '*/.DS_Store' '*/.*' '.*' )
# sanity check: no stray dotfiles in the archive
unzip -l "$ZIP_PATH" | grep -E '(^| +)\.' | grep -vE '\.(html|png|ico|svg|webp|txt|webmanifest|xml|css|js|json)$' \
  && { echo "unsafe dotfiles still present"; exit 1; } || echo "archive clean"

8. Upload to the deploy endpoint

Mirror the headers deploy.sh sends. X-Parkstatic-Wait: true makes the function block until WordPress confirms the install, so a WP-side failure returns non-2xx here.

DEPLOY_URL="https://deploy.parkstatic.site"
PARKSTATIC_SECRET="" # never commit this; export it in the shell instead

GH_SHA=$(git rev-parse HEAD)
GH_REF=$(git rev-parse --abbrev-ref HEAD)
GH_REPO=$(git config --get remote.origin.url | sed 's/\.git$//')

response=$(curl -sS -w '\n__HTTP_STATUS__:%{http_code}' -X POST \
  -H "X-Parkstatic-Token: $PARKSTATIC_SECRET" \
  -H "Content-Type: application/zip" \
  -H "X-Parkstatic-Sha: $GH_SHA" \
  -H "X-Parkstatic-Ref: $GH_REF" \
  -H "X-Parkstatic-Repository: $GH_REPO" \
  -H "X-Parkstatic-Wait: true" \
  --data-binary "@dist.zip" \
  "$DEPLOY_URL")

HTTP_STATUS="${response##*__HTTP_STATUS__:}"
HTTP_BODY="${response%$'\n'__HTTP_STATUS__:*}"
echo "HTTP $HTTP_STATUS"
echo "$HTTP_BODY"

9. Interpret the response

  • 2xx with "ok":true → success. deploy_id is the server-side UUID for the artifact; wp_status: 200 means WordPress confirmed the install. Example:
    {"ok":true,"deploy_id":"0e4b19aa-51d9-428e-94c1-e750b17df6e0","wp_status":200,"message":"WordPress installed 74 files."}
    
  • 401 → X-Parkstatic-Token missing or empty. The Worker didn't forward a token (you forgot to set PARKSTATIC_SECRET, or sent it on the wrong header).
  • 403 → no active paid license. Activate/renew in Parkstatic → Account.
  • 404 → secret did not match any registered Parkstatic instance. Re-copy it from Parkstatic → General → Deploy secret in WP admin, or complete setup first.
  • 400 → malformed upload. If the body says parkstatic_unsafe_zip_entry, re-zip with the dotfile excludes from step 7.
  • 502 → WordPress rejected the deploy. The body carries wp_status and the receiver's error; common Cloudflare origin codes 522/523/524 mean the WP server isn't completing requests.
  • 5xx (other) → Parkstatic storage/signing layer error; usually transient, re-run.

Reference: what the action does that this skill skips

  • Plan step (plan.sh) — sends a repo fingerprint to the plan endpoint and receives a declarative build plan (deps to inject, build kind, whether to prerender). Locally you just run your own build script.
  • Prerender crawl (prerender.sh) — for SSR/SPA builds with no framework prerender, the action boots the server entry (Miniflare for the Cloudflare preset, otherwise a Node fetch server) and crawls routes with headless Chromium to materialise HTML. This local skill does not crawl. Ship a build that already contains final HTML (TanStack Start prerender, Astro static, SvelteKit static adapter, Next.js output: 'export', Nuxt nuxi generate, Vite SPA with a prerender plugin, or a pre-built dist). For SSR-only builds that need crawl materialisation, use the GitHub Action.
  • Dependency injection — the action installs build-only scaffolding (e.g. @tanstack/query-core) into the runner only. Locally your node_modules must already build cleanly.

Verification

After a successful deploy, confirm the live site against your custom domain (the one you mapped to your Parkstatic site in WP admin, not parkstatic.site itself):

SITE_URL="https://your-custom-domain.example.com"

curl -sS -o /dev/null -w 'HTTP %{http_code}\n' -L "$SITE_URL"
curl -sS -L "$SITE_URL" | grep -oE '<title>[^<]*</title>' | head -1

House rules

  • Never commit the secret. Pass it via an env var, a password manager, or an OS keychain. Redact it from logs and transcripts.
  • Always exclude dotfiles before zipping; WordPress rejects .DS_Store and similar.
  • Ship final HTML only. This skill deploys the build output verbatim; it does not prerender. SSR-only builds that need a crawl must use the GitHub Action.
  • One build → one zip → one upload. Don't reuse a stale dist.zip across rebuilds; re-run package + upload each time.
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.07.29

发布时间

Jul 29, 2026

分类

未分类

许可证

NOASSERTION

源路径

parkstatic-local-deploy

默认分支

main

最新提交

5547c20

Tree SHA

7b39160