shiply-cli 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.js +9 -4
- package/dist/functions.js +56 -0
- package/dist/index.js +31 -4
- package/dist/isr.js +50 -0
- package/package.json +1 -1
- package/skill/SKILL.md +31 -21
package/dist/bundle.js
CHANGED
|
@@ -57,12 +57,12 @@ async function bundleWorker(entryAbs, opts = {}) {
|
|
|
57
57
|
* self-contained — `_worker.js` imports server code from sibling dirs
|
|
58
58
|
* (`../output/server`, `../cloudflare-tmp`) — so we bundle it exactly as
|
|
59
59
|
* `wrangler deploy` would, rather than upload it as-is. */
|
|
60
|
-
export async function prepareBundle(sourceDir, ssr) {
|
|
60
|
+
export async function prepareBundle(sourceDir, ssr, extras = {}) {
|
|
61
61
|
const out = await mkdtemp(join(tmpdir(), 'shiply-bundle-'));
|
|
62
62
|
const bundleDir = join(out, '.shiply', 'bundle');
|
|
63
63
|
await mkdir(bundleDir, { recursive: true });
|
|
64
64
|
if (ssr.bundleWith === 'wrangler') {
|
|
65
|
-
await stageWranglerBundle(sourceDir, ssr, out, bundleDir);
|
|
65
|
+
await stageWranglerBundle(sourceDir, ssr, out, bundleDir, extras);
|
|
66
66
|
return out;
|
|
67
67
|
}
|
|
68
68
|
// Bundle the worker entry into one self-contained ESM module.
|
|
@@ -87,6 +87,7 @@ export async function prepareBundle(sourceDir, ssr) {
|
|
|
87
87
|
modules: ['index.js'],
|
|
88
88
|
compatibilityDate: ssr.compatibilityDate ?? BUNDLE_COMPAT_DATE,
|
|
89
89
|
compatibilityFlags: ssr.compatibilityFlags,
|
|
90
|
+
isr: extras.isr,
|
|
90
91
|
});
|
|
91
92
|
return out;
|
|
92
93
|
}
|
|
@@ -94,7 +95,7 @@ export async function prepareBundle(sourceDir, ssr) {
|
|
|
94
95
|
* bundler converts dynamic require('node:*') to top-level imports — esbuild
|
|
95
96
|
* can't), then deploy that worker multi-module with a hand-written asset-first
|
|
96
97
|
* entry. Two modules: index.js (entry) imports ./worker.js (wrangler's bundle). */
|
|
97
|
-
async function stageWranglerBundle(sourceDir, ssr, out, bundleDir) {
|
|
98
|
+
async function stageWranglerBundle(sourceDir, ssr, out, bundleDir, extras = {}) {
|
|
98
99
|
const wtmp = await mkdtemp(join(tmpdir(), 'shiply-wrangler-'));
|
|
99
100
|
// `--dry-run` bundles without deploying (no CF auth needed). Local wrangler
|
|
100
101
|
// (OpenNext projects depend on it) is preferred via npx. shell:true → npx.cmd on Windows.
|
|
@@ -145,9 +146,13 @@ async function stageWranglerBundle(sourceDir, ssr, out, bundleDir) {
|
|
|
145
146
|
modules: ['index.js', 'worker.js'],
|
|
146
147
|
compatibilityDate: ssr.compatibilityDate ?? BUNDLE_COMPAT_DATE,
|
|
147
148
|
compatibilityFlags: ssr.compatibilityFlags,
|
|
149
|
+
isr: extras.isr,
|
|
148
150
|
});
|
|
149
151
|
await rm(wtmp, { recursive: true, force: true });
|
|
150
152
|
}
|
|
151
153
|
async function writeManifest(out, m) {
|
|
152
|
-
|
|
154
|
+
// Drop an undefined `isr` so static/non-Next bundles keep a clean manifest.
|
|
155
|
+
const { isr, ...rest } = m;
|
|
156
|
+
const doc = isr ? { v: 1, ...rest, isr } : { v: 1, ...rest };
|
|
157
|
+
await writeFile(join(out, '.shiply', 'bundle.json'), JSON.stringify(doc, null, 2));
|
|
153
158
|
}
|
package/dist/functions.js
CHANGED
|
@@ -161,6 +161,62 @@ async function removeFnCmd(argv, inherited) {
|
|
|
161
161
|
}
|
|
162
162
|
console.log(`✔ removed worker for ${slug} — static-only routing restored`);
|
|
163
163
|
}
|
|
164
|
+
// --- shiply logs ------------------------------------------------------------
|
|
165
|
+
const LOGS_USAGE = [
|
|
166
|
+
'Usage: shiply logs <slug> [--limit N] [--since MIN] [--json]',
|
|
167
|
+
' shiply logs <slug> # 50 newest events from the last 60 min',
|
|
168
|
+
' shiply logs <slug> --since 1440 # last 24h (max 10080 = 7 days)',
|
|
169
|
+
' shiply logs <slug> --limit 200 --json',
|
|
170
|
+
].join('\n');
|
|
171
|
+
/** Render one log event as a single tail-style line. Pure → unit-testable. */
|
|
172
|
+
export function formatLogLine(e) {
|
|
173
|
+
const ts = new Date(e.timestamp).toISOString();
|
|
174
|
+
const lvl = (e.level || 'info').toUpperCase().padEnd(5);
|
|
175
|
+
const status = e.statusCode ? ` ${e.statusCode}` : '';
|
|
176
|
+
const outcome = e.outcome && e.outcome !== 'ok' ? ` !${e.outcome}` : '';
|
|
177
|
+
const timing = e.wallTimeMs != null ? ` (${e.cpuTimeMs ?? '?'}ms cpu / ${e.wallTimeMs}ms wall)` : '';
|
|
178
|
+
return `${ts} ${lvl} ${e.message}${status}${outcome}${timing}`;
|
|
179
|
+
}
|
|
180
|
+
/** Render a logs payload to the lines `shiply logs` prints: oldest→newest
|
|
181
|
+
* (tail-style, newest at the bottom) + a footer, or a single empty-window hint.
|
|
182
|
+
* The input is newest-first (the API + core both guarantee it), so we reverse.
|
|
183
|
+
* Pure → unit-testable without spawning the process. */
|
|
184
|
+
export function formatLogs(logs, scriptName, slug) {
|
|
185
|
+
if (logs.length === 0) {
|
|
186
|
+
return [
|
|
187
|
+
`no logs for ${slug} in the window — the function may not have been hit recently (try --since 1440)`,
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
const lines = [...logs].reverse().map(formatLogLine);
|
|
191
|
+
lines.push(`\n— ${logs.length} event${logs.length === 1 ? '' : 's'} from ${scriptName}`);
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
/** `shiply logs <slug>` — read recent runtime logs for a site's per-site Worker
|
|
195
|
+
* (Cloudflare Workers Observability, 7-day retention). Prints chronological
|
|
196
|
+
* (newest at the bottom, tail-style); `--json` dumps the raw payload. */
|
|
197
|
+
export async function runLogs(argv, inherited = {}) {
|
|
198
|
+
const { rest, flags } = readFlags(argv, inherited);
|
|
199
|
+
const slug = rest[0];
|
|
200
|
+
if (!slug) {
|
|
201
|
+
console.error(LOGS_USAGE);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const apiKey = await getApiKey(flags, 'logs');
|
|
205
|
+
const base = resolveBase(flags.base);
|
|
206
|
+
const qs = new URLSearchParams();
|
|
207
|
+
if (inherited.limit)
|
|
208
|
+
qs.set('limit', inherited.limit);
|
|
209
|
+
if (inherited.since)
|
|
210
|
+
qs.set('since', inherited.since);
|
|
211
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : '';
|
|
212
|
+
const r = await api(`${base}/api/v1/sites/${slug}/logs${suffix}`, { headers: headers(apiKey) });
|
|
213
|
+
if (flags.json) {
|
|
214
|
+
console.log(JSON.stringify(r, null, 2));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
for (const line of formatLogs(r.logs, r.scriptName, slug))
|
|
218
|
+
console.log(line);
|
|
219
|
+
}
|
|
164
220
|
// --- shiply secret ----------------------------------------------------------
|
|
165
221
|
const SECRET_USAGE = [
|
|
166
222
|
'Usage: shiply secret <set|ls|rm> <slug> [args] [--json]',
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { runDetect } from './detect.js';
|
|
|
10
10
|
import { resolvePayloadDir } from './framework.js';
|
|
11
11
|
import { detectSsr } from './ssr-adapters.js';
|
|
12
12
|
import { prepareBundle } from './bundle.js';
|
|
13
|
+
import { detectIsr } from './isr.js';
|
|
13
14
|
import { runClaimVerify } from './claim.js';
|
|
14
15
|
import { dbAttach, dbBranch, dbBranches, dbCreate, dbDelete, dbDeleteBranch, dbLs, dbMerge, dbMigrate, dbSql, } from './db.js';
|
|
15
16
|
import { driveGet, driveLs, drivePublish, drivePut, driveRm } from './drive.js';
|
|
@@ -18,7 +19,7 @@ import { runProject } from './projects.js';
|
|
|
18
19
|
import { runListing } from './listings.js';
|
|
19
20
|
import { runSendingDomain } from './sending-domains.js';
|
|
20
21
|
import { contractAmend, contractDraft, contractList, contractPdf, contractRetract, contractSend, contractShow, } from './contract.js';
|
|
21
|
-
import { runCron, runFunction, runSecret } from './functions.js';
|
|
22
|
+
import { runCron, runFunction, runLogs, runSecret } from './functions.js';
|
|
22
23
|
import { runEmail } from './email.js';
|
|
23
24
|
import { runMailbox } from './mailbox.js';
|
|
24
25
|
import { loadApiKey, saveApiKey } from './config.js';
|
|
@@ -96,6 +97,9 @@ Usage:
|
|
|
96
97
|
in your worker on next deploy/restart.
|
|
97
98
|
shiply cron <ls|set|rm> <slug> Manage cron triggers on the deployed worker
|
|
98
99
|
shiply cron set <slug> /api/cron/foo "0 9 * * *"
|
|
100
|
+
shiply logs <slug> [--limit N] [--since MIN] Read recent runtime logs for the site's worker
|
|
101
|
+
(Cloudflare Workers Observability, 7-day retention).
|
|
102
|
+
Newest at the bottom; --since in minutes; --json for raw.
|
|
99
103
|
shiply contract <list|draft|show|send|pdf|amend|retract>
|
|
100
104
|
Customer contracts — full lifecycle CLI (B14).
|
|
101
105
|
list <project-id> — parent + amendments table
|
|
@@ -197,7 +201,7 @@ async function main() {
|
|
|
197
201
|
// `--json` is a STRING option (data-insert body). For commands where it's a
|
|
198
202
|
// boolean flag (status/publish/update), detect a BARE --json (no following
|
|
199
203
|
// value) in raw argv and strip it before parseArgs sees it.
|
|
200
|
-
const BARE_JSON_CMDS = new Set(['status', 'publish', 'update']);
|
|
204
|
+
const BARE_JSON_CMDS = new Set(['status', 'publish', 'update', 'logs']);
|
|
201
205
|
const bareJsonFlag = BARE_JSON_CMDS.has(rawArgv[0]) && rawArgv.includes('--json') &&
|
|
202
206
|
!rawArgv.some((a, i) => a === '--json' && typeof rawArgv[i + 1] === 'string' && !rawArgv[i + 1].startsWith('-'));
|
|
203
207
|
if (bareJsonFlag) {
|
|
@@ -235,6 +239,7 @@ async function main() {
|
|
|
235
239
|
'no-confetti': { type: 'boolean' },
|
|
236
240
|
json: { type: 'string' },
|
|
237
241
|
limit: { type: 'string' },
|
|
242
|
+
since: { type: 'string' },
|
|
238
243
|
cursor: { type: 'string' },
|
|
239
244
|
where: { type: 'string' },
|
|
240
245
|
scope: { type: 'string' },
|
|
@@ -283,12 +288,22 @@ async function main() {
|
|
|
283
288
|
let spa;
|
|
284
289
|
let stateDir;
|
|
285
290
|
if (ssr) {
|
|
286
|
-
|
|
291
|
+
// Next ISR demand instrument: measure whether this app asks to
|
|
292
|
+
// revalidate (no infra is provisioned for it yet — those routes serve as
|
|
293
|
+
// live SSR). Records the signal in the bundle for the server-side count
|
|
294
|
+
// and tells the user plainly. Only Next emits a prerender manifest.
|
|
295
|
+
const isr = ssr.framework === 'next' ? ((await detectIsr(dir)) ?? undefined) : undefined;
|
|
296
|
+
stagingDir = await prepareBundle(dir, ssr, { isr });
|
|
287
297
|
publishDir = stagingDir;
|
|
288
298
|
spa = false; // SSR: an asset miss must 404 so the worker handles the route
|
|
289
299
|
stateDir = dir; // persist .shiply.json in the SOURCE dir, not the temp dir
|
|
290
|
-
if (!bareJsonFlag)
|
|
300
|
+
if (!bareJsonFlag) {
|
|
291
301
|
console.log(`✔ ${ssr.framework} (SSR) detected — deploying worker bundle`);
|
|
302
|
+
if (isr && isr.timeBased > 0) {
|
|
303
|
+
console.log(`ℹ Next ISR/revalidate detected on ${isr.timeBased} route(s) (e.g. ${isr.routes[0]}) — ` +
|
|
304
|
+
`shiply serves these as live SSR (re-rendered each request) for now; persistent ISR caching is on the roadmap.`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
292
307
|
}
|
|
293
308
|
else {
|
|
294
309
|
const resolved = await resolvePayloadDir(dir, { framework: values.framework });
|
|
@@ -744,6 +759,18 @@ async function main() {
|
|
|
744
759
|
});
|
|
745
760
|
break;
|
|
746
761
|
}
|
|
762
|
+
case 'logs': {
|
|
763
|
+
// `--json` on `logs` is a BARE flag — stripped from rawArgv before parseArgs
|
|
764
|
+
// (see BARE_JSON_CMDS), so detect it via bareJsonFlag, not values.json.
|
|
765
|
+
await runLogs(positionals.slice(1), {
|
|
766
|
+
base: values.base,
|
|
767
|
+
key: values.key,
|
|
768
|
+
json: bareJsonFlag || (typeof values.json === 'string' && values.json.length > 0),
|
|
769
|
+
limit: values.limit,
|
|
770
|
+
since: values.since,
|
|
771
|
+
});
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
747
774
|
case 'cron': {
|
|
748
775
|
await runCron(positionals.slice(1), {
|
|
749
776
|
base: values.base,
|
package/dist/isr.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const ROUTE_SAMPLE_CAP = 20;
|
|
4
|
+
/** Parse a Next `prerender-manifest.json` and count routes that want time-based
|
|
5
|
+
* revalidation. Pure + total: any malformed/empty input yields zero demand so a
|
|
6
|
+
* publish can never break on it. On-demand revalidation (revalidateTag/Path) is
|
|
7
|
+
* NOT separately detected here — it doesn't live in the prerender manifest. */
|
|
8
|
+
export function scanIsr(prerenderManifestJson) {
|
|
9
|
+
let doc;
|
|
10
|
+
try {
|
|
11
|
+
doc = JSON.parse(prerenderManifestJson);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return { timeBased: 0, routes: [] };
|
|
15
|
+
}
|
|
16
|
+
const buckets = [
|
|
17
|
+
doc?.routes,
|
|
18
|
+
doc?.dynamicRoutes,
|
|
19
|
+
];
|
|
20
|
+
const hits = new Set();
|
|
21
|
+
for (const bucket of buckets) {
|
|
22
|
+
if (!bucket || typeof bucket !== 'object')
|
|
23
|
+
continue;
|
|
24
|
+
for (const [route, meta] of Object.entries(bucket)) {
|
|
25
|
+
const r = meta?.initialRevalidateSeconds;
|
|
26
|
+
if (typeof r === 'number' && Number.isFinite(r) && r > 0)
|
|
27
|
+
hits.add(route);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const routes = [...hits].sort();
|
|
31
|
+
return { timeBased: routes.length, routes: routes.slice(0, ROUTE_SAMPLE_CAP) };
|
|
32
|
+
}
|
|
33
|
+
/** Read a built Next app's prerender manifest and scan it for ISR demand.
|
|
34
|
+
* Returns null when there's no manifest (not a Next build, or build skipped it).
|
|
35
|
+
* Never throws. */
|
|
36
|
+
export async function detectIsr(dir) {
|
|
37
|
+
// `next build` always writes `.next/prerender-manifest.json`; OpenNext keeps
|
|
38
|
+
// `.next` alongside its `.open-next` output. Check both, source-of-truth first.
|
|
39
|
+
for (const rel of ['.next/prerender-manifest.json', '.open-next/.next/prerender-manifest.json']) {
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
text = await readFile(join(dir, rel), 'utf8');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
return scanIsr(text);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: shiply
|
|
3
|
-
description:
|
|
3
|
+
description: "shiply is the production backend for anything an agent builds — publish any app (static or full-stack SSR, any framework) and give it a live URL, custom domain, SQL database, server functions, email, and a marketplace to sell it. Use when the user asks to publish, host, deploy, share, or update a website/page/demo/report/app, or wants a live URL for generated files. Triggers - \"publish this\", \"host this\", \"put this online\", \"give me a link\", \"update the site\", \"deploy my SvelteKit/Astro/Next.js app\", shiply."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
---
|
|
@@ -18,15 +18,17 @@ description: Publish static sites to the web instantly with shiply.now and manag
|
|
|
18
18
|
> fresh — your cached copy may be missing features like databases, projects,
|
|
19
19
|
> marketplace, or sending domains.
|
|
20
20
|
>
|
|
21
|
-
> **Last updated: 2026-06-
|
|
21
|
+
> **Last updated: 2026-06-27**
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
# shiply —
|
|
25
|
+
# shiply — the production backend for anything an agent builds
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
**What it is.** Not static hosting. shiply takes whatever you build — a static page or a full-stack app in any framework — and gives it a live URL, a custom domain, a SQL database, server functions, email (send + receive), and a marketplace to sell it. One call. No account needed to start: anonymous sites are live immediately, last 24 hours, and can be claimed to keep.
|
|
28
|
+
|
|
29
|
+
**Why use it.** Ship a complete, working product without stitching together a host + database + email + registrar + payments — it's one agent-native platform (MCP / CLI / REST parity). Hard-ceiling pricing ($0/$8/$24, no usage meter), so a spike or DDoS can never become a surprise bill for the owner.
|
|
30
|
+
|
|
31
|
+
**How (quickstart).** `shiply publish ./dir` (CLI) — or POST /api/v1/publish → PUT each file → POST finalize — returns a live URL + claimToken.
|
|
30
32
|
|
|
31
33
|
**NEVER create a new site to update an existing one. Always re-publish to the
|
|
32
34
|
same site** — otherwise you litter subdomains and lose the user's URL.
|
|
@@ -144,15 +146,18 @@ so repeat publishes reuse the URL automatically. `--new-site` opts out.
|
|
|
144
146
|
Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
|
|
145
147
|
lines for automation; exit code 0 = ready.
|
|
146
148
|
|
|
147
|
-
### SSR / server frameworks (live:
|
|
149
|
+
### SSR / server frameworks (live: every major JS framework)
|
|
148
150
|
|
|
149
|
-
`shiply publish` auto-detects
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
`shiply publish` auto-detects an SSR build and deploys the worker bundle with
|
|
152
|
+
`nodejs_compat`, serving static assets from the edge. Live and verified on prod:
|
|
153
|
+
SvelteKit (`@sveltejs/adapter-cloudflare`), Astro (`@astrojs/cloudflare`), Qwik
|
|
154
|
+
City, the Nitro family (Nuxt, SolidStart, Analog, TanStack Start), React Router
|
|
155
|
+
v7, and Next.js (via OpenNext) — plus wrangler workers (`wrangler.toml` with
|
|
156
|
+
`main` — Hono, itty-router, raw `fetch`). **Build first** (`npm run build`),
|
|
157
|
+
then `shiply publish .`. Requires sign-in + Developer plan. Force the static
|
|
158
|
+
path with `--framework=<name>`, or skip SSR detection entirely with `--no-ssr`.
|
|
159
|
+
Next.js note: plain SSR works today; ISR / on-demand revalidate is not live yet,
|
|
160
|
+
and Next 16 must build with `next build --webpack` (not Turbopack).
|
|
156
161
|
|
|
157
162
|
## Manage & verify sites (CLI)
|
|
158
163
|
|
|
@@ -557,7 +562,7 @@ secret names (values set via CLI/MCP, never reach the publish payload).
|
|
|
557
562
|
| `remove_function` | Strip function + secrets + crons; fall back to static |
|
|
558
563
|
| `set_secret` / `list_secrets` / `remove_secret` | Manage CF Worker secrets |
|
|
559
564
|
| `set_cron` / `list_crons` / `remove_cron` | Manage cron triggers |
|
|
560
|
-
| `get_function_logs` |
|
|
565
|
+
| `get_function_logs` | Read recent runtime logs (CF Observability, 7-day, newest-first) + a dashboard deep-link |
|
|
561
566
|
|
|
562
567
|
### CLI
|
|
563
568
|
|
|
@@ -567,11 +572,12 @@ shiply function deploy <slug> # alternative: upload worker.js with
|
|
|
567
572
|
shiply function deploy <slug> --ts # uploads worker.ts (server-side compile)
|
|
568
573
|
shiply secret set <slug> STRIPE_KEY sk_xxx # set secret value
|
|
569
574
|
shiply cron set <slug> /api/daily "0 9 * * *"
|
|
575
|
+
shiply logs <slug> # recent worker logs (newest-first); --limit N --since MIN --json
|
|
570
576
|
```
|
|
571
577
|
|
|
572
578
|
### REST
|
|
573
579
|
|
|
574
|
-
`POST/GET/DELETE /api/v1/sites/{slug}/function` · `POST/GET /api/v1/sites/{slug}/secrets` · `DELETE /api/v1/sites/{slug}/secrets/{name}` · `GET/POST/DELETE /api/v1/sites/{slug}/crons`
|
|
580
|
+
`POST/GET/DELETE /api/v1/sites/{slug}/function` · `POST/GET /api/v1/sites/{slug}/secrets` · `DELETE /api/v1/sites/{slug}/secrets/{name}` · `GET/POST/DELETE /api/v1/sites/{slug}/crons` · `GET /api/v1/sites/{slug}/logs?limit&since`
|
|
575
581
|
|
|
576
582
|
### Limits + plan
|
|
577
583
|
|
|
@@ -762,14 +768,18 @@ MCP tool set_site_access does the same. Enforced before any content is served;
|
|
|
762
768
|
changing settings signs current visitors out. Docs: /docs/access-control
|
|
763
769
|
|
|
764
770
|
## Any static site works — frameworks + SSGs auto-detected
|
|
765
|
-
shiply hosts any static site
|
|
766
|
-
|
|
767
|
-
|
|
771
|
+
shiply hosts any static site (and full SSR apps too — see the "SSR / server
|
|
772
|
+
frameworks" section above). For a static build, publish the BUILD OUTPUT, never
|
|
773
|
+
the source: run the build, then publish the output dir with SPA mode for
|
|
774
|
+
client-routed apps. The CLI auto-detects 16+ frameworks and tells you the right command.
|
|
768
775
|
Run `shiply detect` to preview what it found without uploading.
|
|
769
776
|
- Vite (React/Vue/Svelte/Solid/Qwik): `npm run build` then `shiply publish dist --spa`
|
|
770
777
|
- Create React App: `npm run build` then `shiply publish build --spa`
|
|
771
|
-
- Next.js: set `output: "export"` in next.config, `npm run build`, then
|
|
772
|
-
`shiply publish out` (no --spa; export emits real HTML per route)
|
|
778
|
+
- Next.js (static): set `output: "export"` in next.config, `npm run build`, then
|
|
779
|
+
`shiply publish out` (no --spa; export emits real HTML per route). For full
|
|
780
|
+
Next.js SSR (via OpenNext) instead, see the "SSR / server frameworks" section
|
|
781
|
+
above — plain SSR is live (ISR/revalidate not yet; Next 16 needs
|
|
782
|
+
`next build --webpack`).
|
|
773
783
|
- Astro: `npm run build` then `shiply publish dist`
|
|
774
784
|
- SvelteKit (adapter-static): `npm run build` then `shiply publish build --spa`
|
|
775
785
|
- Nuxt: `npx nuxt generate` then `shiply publish .output/public`
|