Add Website Publishing


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, then puts that folder online at https://<name>.puter.site with the hosting API. 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():

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() rejects with a conflict error. Catch it to ask the user for another name, or let puter.randName() pick one nobody has:

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().

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(), keyed by the project:

await puter.kv.set(`site:${project.id}`, site.subdomain);

To find the site later, read it back with puter.kv.get(). It returns null for a project that was never published:

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:

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.

Notes

← All recipes