Skip to content

Added static hosting on profile, poting workflow and LLM Skill.#17

Draft
pmarquees wants to merge 1 commit intohrescak:mainfrom
pmarquees:codex/static-publish-clean-pr
Draft

Added static hosting on profile, poting workflow and LLM Skill.#17
pmarquees wants to merge 1 commit intohrescak:mainfrom
pmarquees:codex/static-publish-clean-pr

Conversation

@pmarquees
Copy link
Copy Markdown
Contributor

PR: Profile-owned static publishing + compose-first workflow for Cursor skills

Summary

This PR adds a full static deployment pipeline for Next.js exported apps, scoped to user profiles, with a compose-first publishing mode designed for non-technical users.

Core outcomes:

  • Static sites are now profile-owned and served at /u/<profileSlug>/<siteSlug>.
  • Legacy /s/<slug> requests are redirected to canonical /u/... when unambiguous.
  • Deploy API supports both service-token and short-lived user-token auth.
  • Compose-first mode deploys and opens /compose with liveUrl prefilled, so users only finish metadata/content and click Publish.
  • Optional auto-post mode still exists for fully automated publishing.

Architecture

Data model

  • User.profileSlug added (unique).
  • StaticSite now belongs to ownerId (User) with unique (ownerId, slug).
  • StaticPublishSession model added for short-lived user publish tokens.
  • StaticDeployment.publishedPostId added to link an auto-created post when enabled.

Routing

  • Canonical host route: GET/HEAD /u/[profileSlug]/[siteSlug]/[[...path]]
  • Backward compatibility route: GET/HEAD /s/[slug]/[[...path]] -> 307 to canonical when unique.
  • Middleware updated so /u/... and /s/... are publicly accessible.

Publish API flow

  1. POST /api/static-sites/init
  2. POST /api/static-sites/sign
  3. Upload files to returned signed URLs
  4. POST /api/static-sites/finalize

Behavior at finalize:

  • createPost=true (default): activates deployment and creates a post.
  • createPost=false: activates deployment only (for compose-first flow).

Auth model

  • Service token auth via STATIC_PUBLISH_TOKEN (server-side secret).
  • User token auth via POST /api/static-sites/session (short-lived token tied to logged-in user).
  • Owner resolution enforces profile ownership for user-issued tokens.

Compose-first support

  • /compose now accepts:
    • ?liveUrl=<url>
    • ?title=<text>
  • Publisher script supports:
    • --post-mode compose
    • --open-compose true

What is required to make this work

  1. Apply migrations:
npx prisma migrate deploy
  1. Ensure R2 is configured:
  • R2_ACCOUNT_ID
  • R2_ACCESS_KEY_ID
  • R2_SECRET_ACCESS_KEY
  • R2_BUCKET_NAME
  • R2_PUBLIC_URL (optional but recommended)
  1. Optional service publishing secret:
  • STATIC_PUBLISH_TOKEN=<hex-token>
  • Recommended generation:
openssl rand -hex 32
  1. For publishing Next.js apps, app must be static export (output: "export") and built with correct base path:
STATIC_BASE_PATH=/u/<profileSlug>/<siteSlug> npm run build

End-user flow

Goal: user runs a skill, lands on /compose, sees Live URL prefilled, adds context/attachments, clicks Publish.

Flow:

  1. Skill builds exported app.
  2. Skill uploads and activates deployment using compose mode (createPost=false).
  3. Skill opens prefilled compose URL.
  4. User finalizes post in Draftboard UI.

How users can create a Cursor skill

Use a project skill that takes:

  • profileSlug
  • siteSlug
  • title
  • projectPath
  • draftboardBaseUrl

Store this secret in Cursor (not in repo):

  • DRAFTBOARD_STATIC_PUBLISH_TOKEN

Skill commands:

cd <projectPath>
STATIC_BASE_PATH=/u/<profileSlug>/<siteSlug> npm run build
curl -fsSL <draftboardBaseUrl>/api/static-sites/publisher-script -o ./draftboard-publish.mjs
node ./draftboard-publish.mjs \
  --base-url <draftboardBaseUrl> \
  --token "$DRAFTBOARD_STATIC_PUBLISH_TOKEN" \
  --profile <profileSlug> \
  --slug <siteSlug> \
  --name "<title>" \
  --post-mode compose \
  --open-compose true \
  --out-dir ./out

Recommended Cursor skill prompt:

Publish the current static Next.js project to Draftboard in compose mode.
Use profileSlug, siteSlug, title, and draftboardBaseUrl inputs.
Build with STATIC_BASE_PATH=/u/<profileSlug>/<siteSlug>.
Run the Draftboard publisher script with --post-mode compose --open-compose true.
Do not print secrets.
If build output path is missing, stop and report the error.

Testing completed

  • npx prisma migrate deploy
  • npx tsc --noEmit
  • npm run build
  • End-to-end deploy tests using real static export:
    • auto-post mode
    • compose mode (verified deployment active and publishedPostId stays null until user publishes manually)

Notes

  • Publisher script now warns when exported asset base path does not match target canonical route.
  • Canonical URL is /u/<profile>/<site>, while /s/<slug> remains compatibility-only.

What this adds
- Introduces profile-owned static sites and canonical hosting URLs: /u/<profileSlug>/<siteSlug>.
- Adds authenticated static publish pipeline endpoints:
  - POST /api/static-sites/init
  - POST /api/static-sites/sign
  - POST /api/static-sites/finalize
- Adds browser helper endpoints:
  - POST /api/static-sites/session (mint short-lived user publish token)
  - GET /api/static-sites/publisher-script (download publisher client)
- Adds compatibility route /s/<slug>/... that redirects to canonical /u/... when unambiguous.
- Adds ownership/auth resolution for publish actors:
  - service token (STATIC_PUBLISH_TOKEN)
  - user-scoped session tokens
- Adds profile slug support end-to-end in auth/user/session.
- Adds deployment metadata and post linkage in Prisma:
  - User.profileSlug (unique)
  - StaticSite owner relation + unique(ownerId, slug)
  - StaticPublishSession model
  - StaticDeployment.publishedPostId relation to Post
- Finalize supports createPost switch:
  - createPost=true (default): auto-create post
  - createPost=false: activate deployment only
- Adds compose-prefill UX:
  - /compose accepts ?liveUrl=<...>&title=<...>
  - publish script compose mode prints (and can auto-open) prefilled compose URL
- Adds CLI publisher script and docs, including basePath validation warnings.

Setup / migration instructions
1. Apply migrations:
   npx prisma migrate deploy
2. Ensure R2 env vars are configured:
   R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME
3. Optional service publish token:
   STATIC_PUBLISH_TOKEN=<hex token>
4. Generate Prisma client if needed:
   npm run db:generate

How users publish static Next.js output
- In exported app, set basePath to the final public URL path (recommended):
  STATIC_BASE_PATH=/u/<profile>/<slug>
- Build app output:
  STATIC_BASE_PATH=/u/<profile>/<slug> npm run build
- Publish with script:
  node draftboard-publish.mjs --base-url <draftboard-url> --token <token> --profile <profile> --slug <slug> --out-dir ./out

Compose-first flow for Cursor skills
- Use compose mode so deployment is activated first and user lands in Draftboard compose with Live URL prefilled:
  node draftboard-publish.mjs     --base-url <draftboard-url>     --token      --profile <profile>     --slug <slug>     --name <title>     --post-mode compose     --open-compose true     --out-dir ./out
- In compose mode:
  - deployment becomes active
  - no post is auto-created
  - script returns /compose?liveUrl=...&title=... and can open it automatically

Cursor skill guidance
- Keep DRAFTBOARD_STATIC_PUBLISH_TOKEN as a skill/runtime secret (never expose to users).
- Skill should:
  1) infer profile + slug
  2) build with STATIC_BASE_PATH=/u/<profile>/<slug>
  3) publish in --post-mode compose
  4) open returned compose URL
- If stronger per-user auth is desired, use /api/static-sites/session tokens instead of shared service token.

Validation performed
- npx prisma migrate deploy
- npx tsc --noEmit
- npm run build
- End-to-end publish runs against /Users/pmarques/Dev/experiments/nextjsthing/out in both auto and compose modes
- Verified compose mode leaves publishedPostId null until user submits compose form
@pmarquees
Copy link
Copy Markdown
Contributor Author

Demo video:

publish.mp4

@pmarquees
Copy link
Copy Markdown
Contributor Author

@hrescak this is a new PR, I closed the old one. Too messy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant