This commit is contained in:
Reynaldi Chernando committed 2026-10-01 01:31:20 +07:00
1 parent 815faaf07f
commit 74fed55958
10 files changed
+159 -304

No files matched your search

-18
View File
@@ -745,12 +745,6 @@ const examples = [
slug: 'hosting-publish-a-website',
source: '/playground/examples/hosting-publish-a-website.html',
},
{
title: 'Release and roll back a site',
description: 'Switch a website between folders to release or roll back a version with Puter.js hosting API. Run and modify this example directly in your browser.',
slug: 'hosting-manage-websites-release',
source: '/playground/examples/hosting-manage-websites-release.html',
},
],
},
{
@@ -998,18 +992,6 @@ const examples = [
slug: 'workers-exec',
source: '/playground/examples/workers-exec.html',
},
{
title: 'Build an API',
description: 'Deploy a worker whose routes store notes in the calling user\'s own key-value store with user.puter, and call it with puter.workers.exec(). Run and modify this example directly in your browser.',
slug: 'workers-build-an-api',
source: '/playground/examples/workers-build-an-api.html',
},
{
title: 'Update a worker in place',
description: 'Ship new code to a deployed worker at the same URL by overwriting its source file with Puter.js workers API. Run and modify this example directly in your browser.',
slug: 'workers-deploy-from-code-update',
source: '/playground/examples/workers-deploy-from-code-update.html',
},
],
},
];
@@ -1,41 +0,0 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<h3>Site dashboard</h3>
<button id="release">Release version 2</button>
<button id="rollback">Roll back to version 1</button>
<button id="delete">Delete site</button>
<script>
const name = puter.randName();
const v1 = `${name}-v1`;
const v2 = `${name}-v2`;
const ready = (async () => {
await puter.fs.mkdir(v1);
await puter.fs.write(`${v1}/index.html`, '<h1>Version 1</h1>');
await puter.fs.mkdir(v2);
await puter.fs.write(`${v2}/index.html`, '<h1>Version 2</h1>');
const site = await puter.hosting.create(name, v1);
const url = `https://${site.subdomain}.puter.site`;
puter.print(`Serving version 1 at <a href="${url}" target="_blank">${url}</a><br>`);
})();
async function serve (dir) {
await ready;
// Same name and URL, different folder.
const site = await puter.hosting.update(name, dir);
puter.print(`Now serving ${site.root_dir.path}<br>`);
}
document.getElementById('release').addEventListener('click', () => serve(v2));
document.getElementById('rollback').addEventListener('click', () => serve(v1));
document.getElementById('delete').addEventListener('click', async () => {
await ready;
await puter.hosting.delete(name);
puter.print('Site deleted. Both folders are still in your account.<br>');
});
</script>
</body>
</html>
@@ -1,62 +0,0 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<h3>Notes</h3>
<input id="text" placeholder="Write a note">
<button id="save">Save note</button>
<button id="list">Show my notes</button>
<script>
// Every note is stored in the calling user's own KV through user.puter,
// so each user only ever sees their own notes.
const workerCode = `
router.post('/notes', async ({ request, user }) => {
if (!user) return new Response('sign in required', { status: 401 });
const { text } = await request.json();
if (typeof text !== 'string' || text.length === 0) {
return new Response('text is required', { status: 400 });
}
const note = { id: crypto.randomUUID(), text, at: Date.now() };
await user.puter.kv.set('notes:' + note.id, note);
return note;
});
router.get('/notes', async ({ user }) => {
if (!user) return new Response('sign in required', { status: 401 });
const rows = await user.puter.kv.list('notes:', true);
return rows.map(row => row.value);
});
`;
let api;
async function deploy () {
await puter.fs.write('notes-api.js', workerCode);
const deployment = await puter.workers.create(puter.randName(), 'notes-api.js');
puter.print(`Deployed to ${deployment.url}, waiting for it to go live...<br>`);
// A new worker takes a few seconds to reach every edge server.
await new Promise(resolve => setTimeout(resolve, 10000));
return deployment.url;
}
document.getElementById('save').addEventListener('click', async () => {
api ??= deploy();
// exec() sends the signed-in user, which is what gives the worker user.puter.
const res = await puter.workers.exec(`${await api}/notes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: document.getElementById('text').value }),
});
puter.print(`${res.status}: ${await res.text()}<br>`);
});
document.getElementById('list').addEventListener('click', async () => {
api ??= deploy();
const notes = await (await puter.workers.exec(`${await api}/notes`)).json();
puter.print(`You have ${notes.length} note(s): ${notes.map(n => n.text).join(', ')}<br>`);
});
</script>
</body>
</html>
@@ -1,41 +0,0 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<h3>App builder: project API</h3>
<input id="greeting" value="Welcome back">
<button id="ship">Ship change</button>
<button id="call">Call the worker</button>
<script>
const name = `project-${puter.randName()}`;
// The route greets whoever calls it, using their own Puter account.
const code = greeting => `
router.get('/', async ({ user }) => {
if (!user) return new Response('sign in required', { status: 401 });
const { username } = await user.puter.getUser();
return ${JSON.stringify(greeting)} + ', ' + username;
});
`;
const deployed = (async () => {
await puter.fs.write(`${name}.js`, code('Hello'));
const { url } = await puter.workers.create(name, `${name}.js`);
puter.print(`Created ${url}, live within about 30 seconds<br>`);
return url;
})();
document.getElementById('ship').addEventListener('click', async () => {
await deployed;
const info = await puter.workers.get(name);
// Overwriting the source file redeploys at the same URL.
await puter.fs.write(info.file_path, code(document.getElementById('greeting').value));
puter.print(`Shipped. ${info.url} serves the new code within about 30 seconds.<br>`);
});
document.getElementById('call').addEventListener('click', async () => {
const res = await puter.workers.exec(await deployed);
puter.print(`${res.status}: ${await res.text()}<br>`);
});
</script>
</body>
</html>
@@ -0,0 +1,121 @@
---
title: Add Website Publishing
description: "Learn how to let users publish websites from your app with Puter.js."
tags: [hosting, fs]
order: 55
---
Some apps let users make something that belongs on the web, like a site
builder or a portfolio maker. When the user is done, they want it online at a
link they can share. Usually that means your app runs its own hosting, with
servers to store every user's files and a domain to serve them from.
With Puter.js, your app publishes the site straight from the browser, like a
static host such as GitHub Pages. It writes the files into a folder in the
user's Puter account with the [filesystem API](/FS/), then puts that folder
online at `https://<name>.puter.site` with the [hosting API](/Hosting/). The
site lives in the user's account, so there is no server for you to run. Anyone
can open the link, with no sign in and no Puter.js.
## Publish What the User Made
Turn the user's project into HTML, write it as `index.html` in a folder, then
pass a site name and the folder to
[`puter.hosting.create()`](/Hosting/create/):
```js
function renderPage (project) {
return `<!doctype html>
<title>${project.title}</title>
<h1>${project.title}</h1>
<p>${project.bio}</p>
<img src="photo.jpg">`;
}
const dir = `sites/${project.id}`;
await puter.fs.write(`${dir}/index.html`, renderPage(project), { createMissingParents: true });
await puter.fs.write(`${dir}/photo.jpg`, project.photo);
const site = await puter.hosting.create('grace-portfolio', dir);
console.log(`https://${site.subdomain}.puter.site`);
```
The site now serves the folder. A request for `/` gets `index.html`, and every
other file is served at its path, such as `photo.jpg` at `/photo.jpg`. Everything
in the folder is public, so write only what the user means to publish.
## Let the User Pick a Name
Site names are shared by every Puter user, so the name the user wants may
already be taken. In that case [`puter.hosting.create()`](/Hosting/create/)
rejects with a `conflict` error. Catch it to ask the user for another name, or
let [`puter.randName()`](/Utils/randName/) pick one nobody has:
```js
async function createSite (dir, name) {
try {
return await puter.hosting.create(name, dir);
} catch (e) {
if (e?.code !== 'conflict') throw e;
return await puter.hosting.create(puter.randName(), dir);
}
}
```
A name can use lowercase letters, digits and hyphens, and cannot start or end
with a hyphen. Check these
rules in your name field before calling
[`puter.hosting.create()`](/Hosting/create/).
## Remember Which Site Belongs to Which Project
When the user comes back to a project, your app needs to know whether it was
published and under which name. Save the site name with
[`puter.kv.set()`](/KV/set/), keyed by the project:
```js
await puter.kv.set(`site:${project.id}`, site.subdomain);
```
To find the site later, read it back with [`puter.kv.get()`](/KV/get/). It
returns `null` for a project that was never published:
```js
const subdomain = await puter.kv.get(`site:${project.id}`);
```
## Publish Again After Edits
A site serves whatever is in its folder, so publishing an edit means writing the
files again. There is nothing to redeploy. This `publish()` function handles
the first publish and every one after it:
```js
async function publish (project, name) {
const dir = `sites/${project.id}`;
await puter.fs.write(`${dir}/index.html`, renderPage(project), { createMissingParents: true });
await puter.fs.write(`${dir}/photo.jpg`, project.photo);
let subdomain = await puter.kv.get(`site:${project.id}`);
if (!subdomain) {
const site = await createSite(dir, name);
subdomain = site.subdomain;
await puter.kv.set(`site:${project.id}`, subdomain);
}
return `https://${subdomain}.puter.site`;
}
```
To list the user's sites or take one offline, see
[Manage websites](/recipes/manage-websites/).
## Notes
- To give single files a public URL, such as images or downloads, see
[Host files online](/recipes/host-file/). One hosted folder serves every file,
with no page or site name to manage.
@@ -1,13 +1,19 @@
---
title: Build an API with a Worker
description: "Learn how to write the routes of a serverless worker with Puter.js, reading requests, storing data in the calling user's own account and sending back JSON or errors."
description: "Learn how to build a backend API for your app with Puter.js serverless workers."
tags: [workers, kv, auth]
order: 38
---
A [serverless worker](/Workers/) is JavaScript that runs in the cloud instead of
in the user's browser. It answers HTTP requests at its own URL, such as
`https://notes-api.puter.work`, and you write it as a list of routes, each one a
Some of your app's code belongs outside the user's browser, such as checking
input before it is saved, receiving a webhook from another service, or calling
another API. That code needs a backend, which usually means running and paying
for a server.
A [serverless worker](/Workers/) gives you that backend with no server to run,
like Cloudflare Workers or AWS Lambda. It is JavaScript that runs in the cloud
and answers HTTP requests at its own URL, such as
`https://notes-api.puter.work`. You write it as a list of routes, each one a
path and the function that handles it.
When your app calls a worker with
@@ -136,8 +142,8 @@ character and does not match anything else.
## Routes Without a User
A worker is also an ordinary HTTP endpoint, so it can serve callers that have
never heard of Puter: a webhook from another service, a `curl` in a script, or a
page that doesn't load Puter.js. These routes never touch `user`.
never heard of Puter. A webhook from another service, a `curl` in a script, or a
page without Puter.js can all call it. These routes never touch `user`.
A route can do its work and answer, with nothing to store:
@@ -184,8 +190,8 @@ router.get('/weather/:city', async ({ params }) => {
});
```
Anyone who knows the URL can call a route like these, so don't put anything
behind one that should be private.
Anyone who knows the URL can call a route like these, so keep private data
behind routes that check for `user`.
## Call It From Your App
@@ -1,17 +1,18 @@
---
title: Deploy a Worker from Your App
description: "Learn how to create, update, list and delete serverless workers from your own code with Puter.js, instead of publishing them by hand."
title: Deploy Workers Programmatically
description: "Learn how to deploy serverless workers from your app with Puter.js, so each user or project gets its own backend."
tags: [workers]
order: 42
---
You can publish a [serverless worker](/Workers/) by hand from puter.com. You
can also do it from code, with the [Workers API](/Workers/#workers-api). Your
app then creates a worker, updates its code and removes it, the same way it
writes files.
Some apps give their users a backend, like an app builder where each project
gets its own API. For that, your app has to deploy the backend from code, at the
moment the user needs it.
This lets you build tools that ship backends for their users, such as an app
builder that gives each project its own API.
With the [Workers API](/Workers/#workers-api), your app creates a
[serverless worker](/Workers/), updates its code and removes it, the same way it
writes files. Each worker answers at its own URL, and there is no server for you
to run.
## Create a Worker
@@ -42,7 +43,7 @@ const res = await puter.workers.exec(`${deployment.url}/me`);
await res.json(); // { username: 'grace' }
```
[Build an API with a worker](/recipes/workers-build-an-api/) covers what to put
[Build an API with a worker](/recipes/build-an-api/) covers what to put
in the routes.
Worker names are global, like subdomains, and are stored in lowercase. A name
@@ -66,9 +67,6 @@ await puter.fs.write(info.file_path, updatedCode);
Writing the file redeploys the worker at the same URL, so anything already
calling it keeps working.
Don't create a new worker with a new name to ship a change. The old one stays
online at its old URL, and every caller has to be pointed at the new one.
## Find Your Workers
To show the workers in your account, call
@@ -124,6 +122,6 @@ await puter.workers.delete('notes-api');
Its URL stops answering, and the name is free to use again. The source file
stays in your account.
The link also works the other way. Deleting a worker's source file with
[`puter.fs.delete()`](/FS/delete/) deletes the worker too, so keep the file for
as long as the worker should stay online.
Deleting a worker's source file with [`puter.fs.delete()`](/FS/delete/) also
deletes the worker, so keep the file for as long as the worker should stay
online.
+6
View File
@@ -101,3 +101,9 @@ To take a link offline, delete the file with the
```js
await puter.fs.delete('public/photo.png');
```
## Notes
- To publish a whole website under a name the user picks, such as a page each
user builds in your app, see
[Add website publishing](/recipes/add-website-publishing/).
+5 -11
View File
@@ -5,7 +5,7 @@ tags: [hosting]
order: 56
---
Once your app has [published a website](/recipes/publish-a-website/), the
Once your app has [published a website](/recipes/add-website-publishing/), the
[hosting API](/Hosting/) lets it find that site again, change which folder it
serves, and take it offline. Each operation is one call that takes the site's
name.
@@ -28,6 +28,10 @@ for (const site of sites) {
Each entry is a [`Subdomain`](/Objects/subdomain/) object. `root_dir` is the
folder it serves, and is `null` when that folder no longer exists.
When your code runs as a Puter app, the list only has the sites that app
created. Sites the user published from somewhere else are left out, and the
other hosting calls cannot change them either.
## Get One Site
To look up a single site by name, call [`puter.hosting.get()`](/Hosting/get/):
@@ -74,13 +78,3 @@ await puter.hosting.delete('grace-portfolio');
The address stops answering. The folder and its files stay in the user's
account, so nothing is lost and you can publish them again later.
## Notes
- When your code runs as a Puter app, these calls only see the sites that app
created. Sites the user published elsewhere are not listed and cannot be
changed.
- [`puter.hosting.list()`](/Hosting/list/) returns sites only. Workers have
their own [`puter.workers.list()`](/Workers/list/).
- For a user with many sites, page through them with the `limit` and `cursor`
options of [`puter.hosting.list()`](/Hosting/list/).
-108
View File
@@ -1,108 +0,0 @@
---
title: Publish a Website
description: "Learn how to publish a website from your app with Puter.js, giving a folder of HTML files its own public puter.site address."
tags: [hosting, fs]
order: 55
---
The [hosting API](/Hosting/) turns a folder in the user's Puter account into a
public website at `https://<name>.puter.site`. Anyone can open it, with no sign
in and no Puter.js. Your app writes the files with the
[filesystem API](/FS/), then publishes the folder with one call.
This lets you build a website builder, a portfolio generator, or an "export as
website" button, with the site hosted in the user's own account.
## Publish a One-Page Site
Write an `index.html` into a folder, then pass a name and the folder to
[`puter.hosting.create()`](/Hosting/create/):
```js
await puter.fs.mkdir('my-site');
await puter.fs.write('my-site/index.html', '<h1>Hello, world!</h1>');
const site = await puter.hosting.create('grace-portfolio', 'my-site');
console.log(`https://${site.subdomain}.puter.site`);
```
The site is now online at that address. A request for `/`, or for any folder,
is answered with that folder's `index.html`.
## Pick a Name
Site names are shared by every Puter user, so a name someone already has is not
free. [`puter.hosting.create()`](/Hosting/create/) then rejects with a
`conflict` error. Catch it and offer another name, or let
[`puter.randName()`](/Utils/randName/) pick one nobody has:
```js
async function publish (dir, name) {
try {
return await puter.hosting.create(name, dir);
} catch (e) {
if (e?.code !== 'conflict') throw e;
return await puter.hosting.create(puter.randName(), dir);
}
}
```
A name can use lowercase letters, digits and hyphens, and cannot start or end
with a hyphen. A few names such as `www` and `api` are reserved.
## Change What It Shows
The site serves whatever is in the folder right now. To change a page, write
the file again. There is nothing to redeploy:
```js
await puter.fs.write('my-site/index.html', '<h1>Hello again!</h1>');
```
The same goes for new files. Anything you add to the folder is online at the
matching path, such as `my-site/about.html` at `/about.html`.
## Serve a Single-Page App
An app with client-side routing needs every unknown path, such as
`/dashboard`, to load `index.html`. Write a
[`.puter_site_config`](/site-config/) file next to it:
```js
await puter.fs.write('my-site/.puter_site_config', JSON.stringify({
errors: { 404: { file: '/index.html', status: 200 } },
}));
```
The config file is never served to visitors. Changes to it take up to a minute
to show.
## Add Dynamic Content
A hosted site is static: it serves the files in the folder as they are. Most
bigger sites also need something that runs on each request, such as saving a
form, loading data per user, or calling an AI model. Put that part in a
[serverless worker](/Workers/) and call it from the site's pages with
[`puter.workers.exec()`](/Workers/exec/):
```js
const res = await puter.workers.exec('https://my-site-api.puter.work/notes');
const notes = await res.json();
```
[Build an API with a worker](/recipes/workers-build-an-api/) shows how to write
those routes. To keep the backend in the same folder as the site, use a
[dynamic worker](/Workers/dynamic/) instead: a file such as
`my-site/__workers/api.worker.js` is served at
`https://<name>.puter.site/__workers/api/`, with no separate deploy.
## Notes
- Everything in the folder is public, including files you add later. Keep
private files somewhere else.
- The user needs a verified email address to publish a site.
- A site's name cannot be changed. To move to a new name, create a site with it
and [delete](/recipes/manage-websites/#delete-a-site) the old one.
- To hand out a link to one file rather than build a site, see
[Host files online](/recipes/host-file/).