Skip to content

feat: ssg:ssh publish target + drop dead legacy SSR code - #36

Merged
vianmora merged 4 commits into
mainfrom
feat/ssh-target-and-ssr-cleanup
Sep 8, 2026
Merged

vianmora merged 4 commits into
mainfrom
feat/ssh-target-and-ssr-cleanup

Conversation

@vianmora

@vianmora vianmora commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Publisher side of webstudio-self-host#7. Three commits:

1. chore: remove dead legacy SSR subprocess code

The react-router-serve subprocess path (state.json mode "ssr") was superseded by one-container-per-site Docker (mode "docker") in #8 and nothing has written mode "ssr" since. Removes ssrProcesses/ssrDomainPort/ssrHostPort, allocateSsrPort (already uncalled), startSsrProcess, stopSsrForDomain, SSR_PORT_BASE, the "ssr" branches in restoreSsrProcesses (→ restoreTargets), all three pipelines' transition blocks, teardownSite, and the proxy's 127.0.0.1 branch. A stale pre-#8 mode:"ssr" state simply isn't restored (404 until republished; unpublish still works).

2. refactor: extract buildSsgOutput()

Pull sync → generate → vike-pin → vite-build → URL-rewrite out of publishBuild into buildSsgOutput({ buildId, domain, workDir, publicOrigin }) returning the dist dir. Also hoist transformOutputFiles() and factor qualifyPublishDomain(). No behaviour change.

3. feat: add ssg:ssh publish target

New host: "ssh"buildSsgOutput() then rsync -az --delete to a remote server. The publisher stops serving locally (container / CF route / Traefik / /var/publish) and writes no Traefik config.

  • Target set once per domain via POST /targets/ssh-setupssh_key (chmod 600, never logged) + target.json + ssh-keyscan'd known_hosts.
  • sshHost/sshUser/sshPath/sshPort validated to safe shapes (they reach ssh-keyscan / rsync as shell args).
  • RENDER_HOSTS["ssg:ssh"], /capabilitiesssh:true + "ssg:ssh" in targets, teardownSite notes remote files kept.
  • Dockerfile: + rsync openssh-client curl jq.

Fork side is already done (host "ssh" has been in the enum since PR #60; the builder's "Remote server (SSH)" option un-greys once capabilities.targets includes ssg:ssh). Supersedes #33.

Test

node --check + smoke tests of availableTargets(), the rsync command string, and the input-validation regexes. No test suite in this repo — end-to-end (ssh-setup → publish → verify on remote) to run on the Coolify bench.

🤖 Generated with Claude Code

https://claude.ai/code/session_017rF3HMB1Z7V3LLsYaUrELs

vianmora and others added 3 commits September 8, 2026 20:30
The react-router-serve subprocess SSR path (state.json mode "ssr") was
superseded by one-container-per-site Docker (mode "docker") in #8, and
nothing has written mode "ssr" since. Remove the orphaned machinery:
ssrProcesses / ssrDomainPort / ssrHostPort maps, allocateSsrPort (already
uncalled), startSsrProcess, stopSsrForDomain, the SSR_PORT_BASE env var,
the "ssr" branches in restoreSsrProcesses (→ restoreTargets), the three
publish pipelines' transition blocks, teardownSite, and the proxy's
127.0.0.1 subprocess branch.

A stale mode:"ssr" state.json (pre-#8 publisher) now simply isn't
restored — the site 404s until republished, and unpublish still tears it
down via the generic hostname cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rF3HMB1Z7V3LLsYaUrELs
…ines

Pull the sync → generate → vike-pin → vite-build → URL-rewrite half of
publishBuild into buildSsgOutput({ buildId, domain, workDir, publicOrigin })
returning the dist dir. publishBuild keeps only the transition handling and
the copy-to-/var/publish (staging + per-custom-domain). A remote pipeline
(SSH, and later Coolify SSG) can now reuse the exact same static build and
just ship the directory elsewhere.

Also hoist the recursive .html/.xml rewriter to a module-level
transformOutputFiles() and factor the repeated
`slug-without-dot → slug.PUBLISHER_HOST` into qualifyPublishDomain(),
both used by all three pipelines. No behaviour change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rF3HMB1Z7V3LLsYaUrELs
New host "ssh": build the site with the shared buildSsgOutput() pipeline,
then `rsync -az --delete` dist/client/ to a remote server over SSH. The
publisher stops serving the hostname locally (kills any container, drops
Cloudflare/Traefik routing, purges /var/publish) and writes no Traefik
config — TLS and web-server config on the remote host are the operator's.

The target (host/user/path/port + private key + optional publicUrl) is set
once per domain via POST /targets/ssh-setup, which stores an ssh_key
(chmod 600, never logged), target.json, and an ssh-keyscan'd known_hosts.
sshHost/sshUser/sshPath/sshPort are validated to safe shapes since they
reach ssh-keyscan and rsync as shell arguments.

RENDER_HOSTS["ssg:ssh"] wired up, /capabilities reports ssh:true and lists
"ssg:ssh" in targets, teardownSite notes the remote files are left in
place. Dockerfile gains rsync + openssh-client (+ curl + jq for calling
the setup endpoint from `docker compose exec`).

Closes webstudio-self-host#7 (publisher side). Supersedes #33.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rF3HMB1Z7V3LLsYaUrELs
…ition

publishBuild never wrote a state.json, so the `prevState.mode === "ssg"`
branches in publishBuildSsr / publishBuildCloudflare (purge the stale
/var/publish copy when a site moves to a runtime) have been dead since
they were added in #8 — switching a published site SSG→SSR left the old
static files on disk, shadowed by the container in the proxy but still
there.

Persist `{ mode: "ssg", publishDomain, customDomains }` at the end of a
successful SSG publish. restoreTargets() already ignores it, teardownSite
falls through to the generic hostname cleanup, and unpublishHostname now
gets a proper hostname list for multi-domain SSG sites instead of the
"no state.json" fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rF3HMB1Z7V3LLsYaUrELs
@vianmora
vianmora merged commit f321c22 into main Sep 8, 2026
1 check passed
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