Saving malleable pages on hyperspace

The hyperspace platform is a place for hosting HTML documents that can modify themselves.

Its power comes from one simple endpoint every HTML site on hyperspace has access to: /save

This endpoint allows pages to replace themselves, which is what makes the magic of self-updating-HTML pages possible.

The save endpoint

fetch('/example-site', {method: 'POST', body: '' + document.documentElement.outerHTML})

As long as you're logged in as the admin of the current site, this is all you need to save a page's content.

Easy save experience

But there's also a hyperspace.js library that make a few things easier.

import {hyperspace} from "/js/hyperspace.js";

hyperspace.initSavePageOnChange(); // 1
hyperspace.initSaveKeyboardShortcut(); // 2

They're pretty self-explanatory.

  1. Saves the page whenever it change
  2. Saves the page when you press the save keyboard shortcut (Ctrl+s or CMD+s).

There's also:

hyperspace.initHyperspaceSaveButton();

Which makes a button like this trigger a save:

<button hyperspace-save="">Save page</button>

As long as you're logged in as the admin of the current site, these methods will set up an easy save experience.

A philosophy of saving

Before your malleable page is saved, we encourage you to make it ready for viewing by non-admin visitors.

This way, random people can load your site and see the default view without any trouble.

Any editable controls or extra stuff you want to load for admin users only can be added dynamically on top of the default view.

Not showing admin-only content

You don't want to show regular visitors to your website all your special admin-only page-editing controls, right?

Well, there's an easy solution.

Hide admin-only content for regular visitors:

<div show-if="admin-page">Only visible to admins</div>

Or, alternatively, hide visitor-only content for admins:

<div show-if="view-page">Only visible to admins</div>

Note: These only control visibility. The content will still be available in the page's source code.

Modify the page before save

Want to completely remove an element from the page before saving?

Well, there's an attribute for that!

<div onbeforesave="this.remove()">Remove me!</div>

Every onbeforesave will be executed before a save occurs.

But let's say you want more control, use beforeSave:

import {hyperspace} from "/js/hyperspace.js";

hyperspace.beforeSave(function (documentElement) {
  // documentElement is a clone of the current page
  documentElement
    .querySelectorAll(".remove")
    .forEach(function (el) { el.remove() });
});

Direct access to save method

If you want to save after a specific event, use this:

import {hyperspace} from "/js/hyperspace.js";

hyperspace.savePage();

This does a few nice things:

  1. Calls any hyperspace.beforeSave callbacks before saving
  2. Executes any onbeforesave attributes
  3. Compares a page's HTML to the previous save and doesn't save it if it's the same HTML
  4. Grabs the page HTML and sends it to the backend

Make form elements persist

One of the coolest things about working with malleable pages is how it feels like you're writing on a piece of paper or drawing on a canvas — everything you do to the page is automatically persisted.

The one exception is form elements, which don't change the structure of the DOM when they're modified by the user.

Here's how to make checkboxes get a checked attribute when they're clicked, inputs change their value when they're typed into, and select elements maintain their selection:

import {hyperspace} from "/js/hyperspace.js";

hyperspace.enablePersistentFormInputValues();

If you pass a universal selector to this method ("*"), it will persist all form elements, but by default it persists form element values with a persist attribute on them.

<input type="checkbox" persist="">

Disable resources before save

Sometimes JS/CSS only applies to an admin view, but you don't want to execute it for regular visitors.

We have a good solution for this:

import {hyperspace} from "/js/hyperspace.js";

hyperspace.disableAdminResourcesBeforeSave();
hyperspace.enableAdminResourcesOnPageLoad();

You need both of these. enableAdminResourcesOnPageLoad will only enable resources for admin users.

Now, just attach a special attribute to your admin-only scripts:

<script admin-resource="" type="admin-resource/module" src="/js/editing/quill.js"></script>

Or styles:

<link admin-resource="" rel="stylesheet" type="text/css" href="/css/quill/editor.css">

These resources will be rendered inert for regular visitors. The JS/CSS will never execute, even though the reference to the resource will remain in the DOM.

Why would you want to do this?

  1. There are not 2 versions of your page, one for admins and one for viewers. Your page must always contain references to all the resources it needs to become either type of page.
  2. Your page will be able to shift into admin-mode or view-only mode, making it useful to anyone who views or downloads it and wants to use it locally or as a basis for their own site.

Disable contenteditable before save

contenteditable is great for making an element editable quickly, but you often only want admin users editing stuff.

Here's how you can add a bunch of contenteditables to your page and have them rendered inert when regular visitors see it:

import {hyperspace} from "/js/hyperspace.js";

hyperspace.disableContentEditableBeforeSave();
hyperspace.enableContentEditableForAdminOnPageLoad();

Disable onclick before save

onclick is great for making your page interactive, especially if you use jQuery or the all.js library we cooked up, but you might not need onclick for regular visitors.

Here's how you can use onclick for admin users, but render them inert for regular visitors:

import {hyperspace} from "/js/hyperspace.js";

hyperspace.disableOnClickBeforeSave();
hyperspace.enableOnClickForAdminOnPageLoad();

Upload files

You can upload a file from the main sites dashboard or from the code editor, but if you want to do it programmatically from a site, here's how:

<input type="file" onchange="hyperspace.uploadFile(event)">

Or:

hyperspace.uploadFile(event).then(function (result) {
  console.log('File uploaded:', result.url);
});

You can also create a file by passing in strings:

<form onsubmit="hyperspace.createFile(event)">
  <input name="file_name" value="example.txt">
  <textarea name="file_body">File content here</textarea>
</form>

Or:

hyperspace.createFile('example.txt', 'Content here').then(function (result) {
  console.log('File created:', result.url);
});

These methods will all:

  1. Show progress notifications while uploading
  2. Copy the fil URL to clipboard on success
  3. Reset form inputs after successful upload

If you'd rather handle the single-file upload yourself, you can use uploadFileBasic:

// With a file input
fileInput.addEventListener('change', function (event) {
  hyperspace.uploadFileBasic(event, {
    onProgress: function (percent) console.log(`${percent}% complete`),
    onComplete: function (url) console.log(`Uploaded to: ${url}`),
    onError: function (error) console.error('Upload failed:', error)
  });
});

// With a File object
const file = new File(['content'], 'test.txt');
hyperspace.uploadFileBasic(file, {
  onProgress: function (percent) console.log(`${percent}% complete`),
  onComplete: function (url) console.log(`Uploaded to: ${url}`),
  onError: function (error) console.error('Upload failed:', error)
});