Add OpenClaw web pages with static/dynamic split and prod deploy workflow#485
Add OpenClaw web pages with static/dynamic split and prod deploy workflow#485willwashburn merged 13 commits intomainfrom
Conversation
Introduce an isolated SST app under openclaw-web: add sst.config.ts to provision an OpenClaw Function (with .md text loader) and a Cloudflare-backed Router for production. Add a Lambda handler (src/openclaw.ts) that serves packages/openclaw/skill/SKILL.md as HTML and injects explicit invite_token/workspace key instructions when provided. Add markdown.d.ts, update .gitignore to ignore SST artifacts, and add a GitHub Actions workflow (deploy-openclaw-page.yml) to deploy the page on pushes to main. Update package.json/lock to include SST-related dev deps/scripts and add a set of trajectory metadata files documenting the changes.
Migrate openclaw-web to a Next.js app: add app/, components, lib/skill-markdown.ts, globals.css, next.config.mjs, next-env and package.json to serve /openclaw (static) and /openclaw/invite/[token] (SSR). Replace the prior static build script and inline markdown import with a helper that reads the canonical packages/openclaw/skill/SKILL.md and applies invite tokens; enable outputFileTracingIncludes so Next bundles the external SKILL.md. Remove legacy build/static sources and markdown typings, update SST/Next config and top-level package metadata, and add trajectory records reflecting routing and SKILL decisions. Also tweak .gitignore (remove openclaw-web/site/ line).
Reorder OpenClaw SKILL.md so "Create New Workspace" is step 1 and move "Join Existing Workspace" to step 2. Update openclaw-web/lib/skill-markdown.ts to stop wholesale replacement of the create-workspace block when an invite token is present — it now adds an advisory note under the create-workspace heading and only customizes the join-existing command/intro with the provided token. Also add trajectory records and update the .trajectories index. This keeps SKILL.md authoritative and avoids drifting rendered docs when tokens are supplied.
|
Addressed both inline review findings in aa5f547:\n- switched setup-command substitution to |
# Conflicts: # packages/openclaw/skill/SKILL.md
There was a problem hiding this comment.
Pull request overview
Adds a new openclaw-web Next.js app to publish the canonical OpenClaw SKILL content at /openclaw (static) and /openclaw/invite/[token] (dynamic), along with SST configuration and a production deploy workflow.
Changes:
- Introduces
openclaw-web(Next.js) with static/dynamic routes that renderpackages/openclaw/skill/SKILL.md, plus token-based instruction injection. - Updates
packages/openclaw/skill/SKILL.mdto reorder setup steps and switch invite URLs to the new path-based route. - Adds SST config + a GitHub Actions workflow to deploy the OpenClaw page on merges to
main, and wires the app into the monorepo workspaces.
Reviewed changes
Copilot reviewed 52 out of 55 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/openclaw/skill/SKILL.md | Reorders setup sections and updates invite URLs to /openclaw/invite/<token>; copy tweaks. |
| package.json | Adds openclaw-web workspace, adds dev:web script, adds sst devDependency. |
| package-lock.json | Lockfile updates for new workspace + sst/Next dependencies. |
| openclaw-web/tsconfig.json | Adds TS config for the Next.js app. |
| openclaw-web/sst.config.ts | Defines SST Router + Nextjs component and production domain settings. |
| openclaw-web/package.json | Declares Next/React dependencies and basic scripts. |
| openclaw-web/next.config.mjs | Configures output file tracing to include external SKILL.md. |
| openclaw-web/next-env.d.ts | Next TypeScript environment declarations for the app. |
| openclaw-web/lib/skill-markdown.ts | Reads SKILL.md and injects invite-token specific instructions via string transforms. |
| openclaw-web/components/SkillPage.tsx | Renders the page wrapper and displays the markdown content. |
| openclaw-web/app/page.tsx | Root route returns 404 via notFound(). |
| openclaw-web/app/openclaw/page.tsx | Static /openclaw route rendering canonical SKILL.md. |
| openclaw-web/app/openclaw/invite/[token]/page.tsx | Dynamic invite route that applies token-specific markdown transformations. |
| openclaw-web/app/layout.tsx | Sets basic HTML layout + page metadata and imports global styles. |
| openclaw-web/app/globals.css | Adds basic styling for the rendered page. |
| .trajectories/index.json | Updates trajectory index timestamp and adds entries for the work done in this PR. |
| .trajectories/completed/2026-03/traj_zqrckxk72crk.md | Records a completed trajectory related to route restructuring. |
| .trajectories/completed/2026-03/traj_zqrckxk72crk.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_ydyvruawxtxy.md | Records a completed trajectory for migrating to Next.js. |
| .trajectories/completed/2026-03/traj_ydyvruawxtxy.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_v2w5gavdktck.md | Records a completed trajectory for adding SST + dev script changes. |
| .trajectories/completed/2026-03/traj_v2w5gavdktck.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_twbd14s960dc.md | Records a completed trajectory for pinning Next.js version. |
| .trajectories/completed/2026-03/traj_twbd14s960dc.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_ttciubcdm460.md | Records a completed trajectory for static/dynamic route split + URL updates. |
| .trajectories/completed/2026-03/traj_ttciubcdm460.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_tt4dr55gmhpv.md | Records a completed trajectory for reordering setup steps. |
| .trajectories/completed/2026-03/traj_tt4dr55gmhpv.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_sl9f8uw68x4p.md | Records a completed trajectory related to /openclaw redirect handling. |
| .trajectories/completed/2026-03/traj_sl9f8uw68x4p.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_raauoa7kxj1h.md | Records a completed trajectory related to routing origin errors. |
| .trajectories/completed/2026-03/traj_raauoa7kxj1h.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_pxfql40zgwuv.md | Records a completed trajectory for running SST dev preview. |
| .trajectories/completed/2026-03/traj_pxfql40zgwuv.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_l2lriwaf3e9f.md | Records a completed trajectory related to sourcing SKILL.md. |
| .trajectories/completed/2026-03/traj_l2lriwaf3e9f.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_iz9iq4300df1.md | Records a completed trajectory for recording SKILL sync decision. |
| .trajectories/completed/2026-03/traj_iz9iq4300df1.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_isz7r4chot7w.md | Records a completed trajectory for invite-token rendering adjustments. |
| .trajectories/completed/2026-03/traj_isz7r4chot7w.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_im6qroacmhka.md | Records a completed trajectory for routing decision documentation. |
| .trajectories/completed/2026-03/traj_im6qroacmhka.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_gulmgt0rg4dj.md | Records a completed trajectory for removing SKILL copy step. |
| .trajectories/completed/2026-03/traj_gulmgt0rg4dj.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_dkisssqguz7b.md | Records a completed trajectory for moving token instructions into setup steps. |
| .trajectories/completed/2026-03/traj_dkisssqguz7b.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_66gxsc0fhsem.md | Records a completed trajectory for initial SST Lambda URL approach. |
| .trajectories/completed/2026-03/traj_66gxsc0fhsem.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_23jmoytnrrxc.md | Records a completed trajectory for copy/badge behavior adjustments. |
| .trajectories/completed/2026-03/traj_23jmoytnrrxc.json | Trajectory metadata for the same completed task. |
| .trajectories/completed/2026-03/traj_1b88m050m7vd.md | Records a completed trajectory for explicit invite token instructions. |
| .trajectories/completed/2026-03/traj_1b88m050m7vd.json | Trajectory metadata for the same completed task. |
| .husky/pre-commit | Sources NVM (if present) before running lint-staged. |
| .gitignore | Ignores SST artifacts (.sst, openclaw-web/sst-env.d.ts). |
| .github/workflows/deploy-openclaw-page.yml | Adds production deployment workflow for the SST/Next app on main. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| CREATE_WORKSPACE_HEADING, | ||
| [CREATE_WORKSPACE_HEADING, '', "Since you have a key, you don't need to setup a new workspace."].join( | ||
| '\n' | ||
| ) |
There was a problem hiding this comment.
The injected note uses "don't need to setup"; as a verb phrase this should be "set up". Since this is user-facing copy on the invite page, update the wording to avoid the grammatical error.
| .replaceAll(JOIN_WORKSPACE_COMMAND, `npx -y @agent-relay/openclaw setup ${token} --name my-claw`) | ||
| .replaceAll('Enter your workspace key (`rk_live_...`)', 'Open the shared workspace in observer'); |
There was a problem hiding this comment.
The replacement for the observer instructions changes Enter your workspace key (rk_live_...) to Open the shared workspace in observer, but the resulting sentence still reads "...to authenticate" without telling the user what key to enter. Consider replacing that phrase with token-specific guidance (e.g., instruct to enter the provided key) rather than removing the key-entry instruction entirely.
| export function applyInviteToken(markdown: string, inviteToken: string): string { | ||
| const token = inviteToken.trim(); | ||
| if (!token) return markdown; | ||
|
|
||
| const joinWorkspaceIntro = [ |
There was a problem hiding this comment.
applyInviteToken is a string-based transformation that’s easy to break when SKILL.md headings/phrasing change, and there are no automated tests covering the invite-token rendering. Add a small test suite for applyInviteToken (and ensure it runs in CI—root Vitest config currently doesn’t include openclaw-web/**) so regressions are caught automatically.
| ? { | ||
| domain: { | ||
| name: 'agentrelay.net', | ||
| dns: sst.cloudflare.dns({ proxy: true }), | ||
| }, |
There was a problem hiding this comment.
The SST custom domain is set to agentrelay.net, but the canonical URLs in packages/openclaw/skill/SKILL.md point to https://agentrelay.dev/openclaw/.... Unless there is an intentional redirect/CNAME strategy, this mismatch will cause the deployed site to not match the documented invite URLs. Align the deployed domain with the documented one (or update the docs/redirects accordingly).
|
|
||
| permissions: | ||
| contents: read | ||
| id-token: write |
There was a problem hiding this comment.
This workflow grants id-token: write but authenticates to AWS using long-lived access keys (not OIDC). If OIDC isn’t intended, drop the id-token permission (least privilege). If OIDC is intended, switch configure-aws-credentials to role-to-assume and remove the static AWS secrets.
| id-token: write |
| type PageProps = { | ||
| params: Promise<{ | ||
| token: string; | ||
| }>; | ||
| }; | ||
|
|
||
| export default async function InvitePage({ params }: PageProps) { | ||
| const { token } = await params; | ||
| const inviteToken = decodeURIComponent(token).trim(); |
There was a problem hiding this comment.
params for App Router pages is an object, not a Promise. Typing it as Promise<{ token: string }> and awaiting it is likely to break type-checking and can mask real issues. Change PageProps to params: { token: string } and remove the await.
| export default async function InvitePage({ params }: PageProps) { | ||
| const { token } = await params; | ||
| const inviteToken = decodeURIComponent(token).trim(); | ||
| if (!inviteToken) notFound(); |
There was a problem hiding this comment.
decodeURIComponent(token) can throw a URIError for malformed percent-encoding (e.g. a token containing % sequences). Right now that would turn into a 500; wrap the decode in a try/catch and call notFound() (or otherwise handle) on decode failure.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 55 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
openclaw-web/lib/skill-markdown.ts:67
applyInviteToken()attempts to replace the stringEnter your workspace key (rk_live_...), but that text does not exist in the currentpackages/openclaw/skill/SKILL.md. This replacement is effectively dead code and makes the transform harder to reason about; please remove it or update it to match the actual source markdown.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| return markdown | ||
| .replace(CREATE_WORKSPACE_HEADING, () => [CREATE_WORKSPACE_HEADING, '', SETUP_SKIP_NOTE].join('\n')) | ||
| .replace(JOIN_WORKSPACE_LINE, () => | ||
| ['Use this shared workspace key so all claws join the same workspace:', '', joinWorkspaceIntro].join( |
There was a problem hiding this comment.
applyInviteToken() appends a note under the “Create New Workspace” heading, but it doesn't explicitly tell the reader to skip running the Step 1 command (which is still shown immediately below). Consider making the inserted note unambiguous (eg explicitly “Skip Step 1 and proceed to Step 2” when a token is provided) to prevent accidentally creating a new workspace.
| ? { | ||
| domain: { | ||
| name: 'agentrelay.net', | ||
| dns: sst.cloudflare.dns({ proxy: true }), | ||
| }, | ||
| } |
There was a problem hiding this comment.
Docs in packages/openclaw/skill/SKILL.md and the PR description reference https://agentrelay.dev/openclaw/..., but the production custom domain configured here is agentrelay.net. Please confirm the intended public hostname and align either the SST domain config or the documented invite/homepage URLs so generated links don’t point at the wrong site.
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| deploy: | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| AWS_REGION_INPUT: ${{ github.event_name == 'workflow_dispatch' && inputs.region || 'us-east-1' }} | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '22' | ||
| cache: 'npm' | ||
|
|
||
| - name: Install dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Validate Cloudflare token | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| run: | | ||
| if [ -z "${CLOUDFLARE_API_TOKEN}" ]; then | ||
| echo "Missing required secret: CLOUDFLARE_API_TOKEN" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Configure AWS credentials | ||
| uses: aws-actions/configure-aws-credentials@v4 | ||
| with: | ||
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | ||
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | ||
| aws-region: ${{ env.AWS_REGION_INPUT }} | ||
|
|
There was a problem hiding this comment.
This workflow requests id-token: write but then configures AWS using long-lived AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY secrets. Consider switching to GitHub OIDC (role-to-assume) to avoid static AWS keys, or remove the unused id-token permission if static credentials are intentional.
| ## 2) Setup (Join Existing Workspace) | ||
|
|
||
| Use a shared workspace key (`rk_live_...`) so all claws join the same workspace: |
There was a problem hiding this comment.
The setup commands are now inconsistent: Step 1 uses @agent-relay/openclaw@latest, but Step 2 uses @agent-relay/openclaw without @latest. This can confuse users and can also change which version npx runs depending on local caching. Consider using @latest consistently for all npx examples (or consistently omitting it).
| ```bash | ||
| npx -y @agent-relay/openclaw@latest status | ||
| npx -y @agent-relay/openclaw@latest help |
There was a problem hiding this comment.
This troubleshooting snippet uses npx -y @agent-relay/openclaw ... while surrounding sections use @agent-relay/openclaw@latest. To avoid ambiguity (and to match the earlier docs), make the version qualifier consistent across the guide.
| const SETUP_SKIP_NOTE = "Since you have a key, you don't need to set up a new workspace."; | ||
|
|
||
| export function readSkillMarkdown(): string { |
There was a problem hiding this comment.
readSkillMarkdown() reads SKILL.md synchronously from disk every call. On the force-dynamic invite route, this will happen on every request. Since the content is static at runtime, consider reading once at module load (or memoizing) and reusing the cached string to reduce per-request I/O.
|
Addressed the latest review comments in d29409e:
Note on |
# Conflicts: # packages/openclaw/skill/SKILL.md
|
Addressed the latest review comments in 325cf95:
Also already fixed earlier:
Left intentionally unchanged:
|
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 55 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| paths: | ||
| - openclaw-web/** | ||
| - packages/openclaw/skill/SKILL.md | ||
| - .github/workflows/deploy-openclaw-page.yml |
There was a problem hiding this comment.
The workflow is path-filtered to openclaw-web/** and SKILL.md, but the deploy uses npm ci at the repo root and runs the root-installed sst binary. Changes to root package.json / package-lock.json (e.g. SST version bumps) can affect the deployed output but will not trigger this workflow. Consider adding package.json and package-lock.json (and any other relevant root build files) to the on.push.paths list.
| - .github/workflows/deploy-openclaw-page.yml | |
| - .github/workflows/deploy-openclaw-page.yml | |
| - package.json | |
| - package-lock.json |
| "traj_66gxsc0fhsem": { | ||
| "title": "Create SST Lambda URL serving OpenClaw skill with invite-token behavior", | ||
| "status": "completed", | ||
| "startedAt": "2026-03-04T21:53:53.494Z", | ||
| "completedAt": "2026-03-04T21:56:15.548Z", | ||
| "path": "/Users/will/Projects/relay/.trajectories/completed/2026-03/traj_66gxsc0fhsem.json" | ||
| }, |
There was a problem hiding this comment.
New trajectory entries include absolute local filesystem paths (e.g. /Users/will/Projects/relay/...). These paths are non-portable and can leak developer machine details; prefer storing paths relative to the repo root (or omit them) so the data is consistent across environments.
| const SKILL_PATH = resolveSkillPath(); | ||
| const SKILL_MARKDOWN = fs.readFileSync(SKILL_PATH, 'utf8'); | ||
|
|
||
| const JOIN_WORKSPACE_LINE = | ||
| 'Use a shared workspace key (`rk_live_...`) so all claws join the same workspace:'; | ||
| const CREATE_WORKSPACE_HEADING = '## 1) Setup (Create New Workspace)'; | ||
| const OBSERVER_AUTH_LINE = 'Authenticate with workspace key (`rk_live_...`).'; | ||
| const TOKEN_PLACEHOLDER = 'rk_live_YOUR_WORKSPACE_KEY'; | ||
| const SETUP_SKIP_NOTE = 'Since you already have a workspace key, skip Step 1 and continue with Step 2 below.'; | ||
|
|
||
| export function readSkillMarkdown(): string { | ||
| return SKILL_MARKDOWN; | ||
| } |
There was a problem hiding this comment.
SKILL.md is read and cached at module load (SKILL_MARKDOWN). With ISR (revalidate) and in local dev, updates to packages/openclaw/skill/SKILL.md won't be picked up until a full process restart/cold start, which defeats the intent of revalidation. Consider reading the file inside readSkillMarkdown() (optionally with a small in-memory cache keyed by mtime) instead of caching the contents at import time.
| Humans can watch workspace conversation at: | ||
| <https://agentrelay.dev/observer> | ||
| <https://agentrelay.net/observer> | ||
|
|
||
| Authenticate with workspace key (`rk_live_...`). |
There was a problem hiding this comment.
This guide now points the Observer URL at https://agentrelay.net/observer, but the SDK/runtime generates observer links under https://observer.relaycast.dev/?key=... and the new Next app in this PR does not define a /observer route. Unless there is an external redirect already configured, this link will 404. Please update the doc to the correct Observer URL (or add/provision a /observer route/redirect on agentrelay.net).
Add a new OpenClaw landing page with scoped styles and a copy-instructions button, convert SkillPage to use scoped .skill-page styles, and add a dedicated /openclaw/skill route. Standardize invite and observer URLs to agentrelay.dev across SKILL.md, SDK (observerUrl/log output), and docs/examples. Update sst config to attach the production domain and simplify Next.js deployment options. Minor edits: global CSS cleanup, Next types import fix, route file rename for invite path, updated unit test expectations, and trajectory metadata files for the change trace.
| async function handleCopy() { | ||
| await navigator.clipboard.writeText(INSTRUCTIONS_TEXT); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); | ||
| } |
There was a problem hiding this comment.
🟡 Unhandled promise rejection when clipboard API is unavailable
The handleCopy async function in CopyInstructionsButton awaits navigator.clipboard.writeText() without any error handling. The Clipboard API throws when called in non-secure contexts (plain HTTP), when the browser permission is denied, or when navigator.clipboard is undefined. Since React's onClick doesn't catch rejections from async handlers, this produces an unhandled promise rejection and the button silently fails without any user feedback.
| async function handleCopy() { | |
| await navigator.clipboard.writeText(INSTRUCTIONS_TEXT); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| } | |
| async function handleCopy() { | |
| try { | |
| await navigator.clipboard.writeText(INSTRUCTIONS_TEXT); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| } catch { | |
| // Clipboard API unavailable (non-secure context, permission denied, etc.) | |
| } | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
openclaw-webas a Next.js app (Next 16) with routes:/openclaw(static, cacheable render ofpackages/openclaw/skill/SKILL.md)/openclaw/invite/[token](dynamic render with explicit token instructions)packages/openclaw/skill/SKILL.md(no copied skill content)openclaw-web/sst.config.tsmainNotes
/returns 404; intended routes are/openclawand/openclaw/invite/[token]Validation
next devroutes for/openclawand/openclaw/invite/[token]