diff --git a/src/docs/src/examples.js b/src/docs/src/examples.js index b083ec088..a42f13f17 100644 --- a/src/docs/src/examples.js +++ b/src/docs/src/examples.js @@ -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', - }, ], }, ]; diff --git a/src/docs/src/playground/examples/hosting-manage-websites-release.html b/src/docs/src/playground/examples/hosting-manage-websites-release.html deleted file mode 100644 index 529306a52..000000000 --- a/src/docs/src/playground/examples/hosting-manage-websites-release.html +++ /dev/null @@ -1,41 +0,0 @@ - - - -

Site dashboard

- - - - - - diff --git a/src/docs/src/playground/examples/workers-build-an-api.html b/src/docs/src/playground/examples/workers-build-an-api.html deleted file mode 100644 index e43c2bcf1..000000000 --- a/src/docs/src/playground/examples/workers-build-an-api.html +++ /dev/null @@ -1,62 +0,0 @@ - - - -

Notes

- - - - - - diff --git a/src/docs/src/playground/examples/workers-deploy-from-code-update.html b/src/docs/src/playground/examples/workers-deploy-from-code-update.html deleted file mode 100644 index af54be5d7..000000000 --- a/src/docs/src/playground/examples/workers-deploy-from-code-update.html +++ /dev/null @@ -1,41 +0,0 @@ - - - -

App builder: project API

- - - - - - diff --git a/src/docs/src/recipes/add-website-publishing.md b/src/docs/src/recipes/add-website-publishing.md new file mode 100644 index 000000000..1c4cb9577 --- /dev/null +++ b/src/docs/src/recipes/add-website-publishing.md @@ -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://.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 ` +${project.title} +

${project.title}

+

${project.bio}

+`; +} + +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. diff --git a/src/docs/src/recipes/workers-build-an-api.md b/src/docs/src/recipes/build-an-api.md similarity index 87% rename from src/docs/src/recipes/workers-build-an-api.md rename to src/docs/src/recipes/build-an-api.md index ad4fe1fe5..d3f057a98 100644 --- a/src/docs/src/recipes/workers-build-an-api.md +++ b/src/docs/src/recipes/build-an-api.md @@ -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 diff --git a/src/docs/src/recipes/workers-deploy-from-code.md b/src/docs/src/recipes/deploy-workers-programmatically.md similarity index 76% rename from src/docs/src/recipes/workers-deploy-from-code.md rename to src/docs/src/recipes/deploy-workers-programmatically.md index f99ff11af..56796e751 100644 --- a/src/docs/src/recipes/workers-deploy-from-code.md +++ b/src/docs/src/recipes/deploy-workers-programmatically.md @@ -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. diff --git a/src/docs/src/recipes/host-file.md b/src/docs/src/recipes/host-file.md index 442c13c89..ea4b8556e 100644 --- a/src/docs/src/recipes/host-file.md +++ b/src/docs/src/recipes/host-file.md @@ -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/). diff --git a/src/docs/src/recipes/manage-websites.md b/src/docs/src/recipes/manage-websites.md index 016d906c0..250f52bab 100644 --- a/src/docs/src/recipes/manage-websites.md +++ b/src/docs/src/recipes/manage-websites.md @@ -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/). diff --git a/src/docs/src/recipes/publish-a-website.md b/src/docs/src/recipes/publish-a-website.md deleted file mode 100644 index afc367dda..000000000 --- a/src/docs/src/recipes/publish-a-website.md +++ /dev/null @@ -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://.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', '

Hello, world!

'); - -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', '

Hello again!

'); -``` - -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://.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/).