Writing an HTML5 page in Celestory

๐Ÿง‘โ€๐Ÿ’ป Writing an HTML5 page in Celestory


The HTML5 block is Celestory's escape hatch. You paste a complete web page into it โ€” HTML, CSS, JavaScript โ€” and Celestory shows it full screen at that point of the journey. A mini-game, a bespoke form, a simulation, a hand-drawn canvas: anything there is no block for.


But a page on its own is useless. What matters is the contract: how the page receives values from the graph, and how it hands values back. It comes down to three things. ๐Ÿ”Œ


๐Ÿ”บ The HTML5 block requires a Business subscription.



๐Ÿ“ Where you write, and when it is saved


Open the HTML5 block (double-click). Its content input hosts a full code editor โ€” the Visual Studio Code engine โ€” with syntax highlighting and folding.


๐Ÿ”บ Your code only reaches the graph when the editor loses focus. While the caret is still blinking in the editor, Celestory sees nothing. Click outside the editor, or close the block's edit window: that is the moment the code is saved and the input and output points appear on, or disappear from, the block.


This is confusion number one. If you have just pasted a {{firstname}} and no input shows up, you simply have not left the editor yet. ๐Ÿ™‚


The page must be self-contained: nothing is bundled next to it and no neighbouring file is served. Everything it needs lives inside the block, or loads from a public URL.



๐Ÿ“ฅ Receiving values: double curly braces


Write {{name}} anywhere in the code and Celestory creates an input of that name on the block.


<h1>Hello {{firstname}}!</h1>
<p>You are on {{score}} points.</p>


Two inputs appear: firstname and score. Wire a variable to them, or another block's output, or type a fixed value. They accept Text, a Number or a Boolean.


๐Ÿ”บ Only unaccented letters, digits and the underscore are accepted: a-z, A-Z, 0-9, _. {{Prรฉnom}}, {{my-score}} and {{ firstname }} are not detected. No input is created, no error is raised, and the marker is displayed on screen as written. Case matters. These are exactly the rules covered in the double curly braces article.


๐Ÿ”บ The substitution is purely textual, performed on the source before the page runs. In JavaScript, a text value therefore needs its quotes:


<script>
const points = {{score}}; // โœ… a number, as is
const player = "{{firstname}}"; // โœ… a text, in quotes
const broken = {{firstname}}; // โŒ syntax error at runtime
</script>


๐Ÿ”บ An input left unwired is replaced by nothing at all. const points = ; โ€” your page never starts, and nothing warns you.



๐Ÿ“ค Handing values back: declare, then post


This is the least obvious part, and it happens in two steps.


1. Declare what the page will return


Write this comment anywhere in the page:


<!-- celestory-outputs: score, verdict -->


Each listed name becomes an output on the block, which you can read, test or wire like any other. Separate them with commas or spaces, on one line or several. These outputs carry Text, a Number or a Boolean.


๐Ÿ”บ The same naming rules apply: unaccented letters, digits, underscore. Nothing else.


2. Post the value while the page is running


<script>
parent.document.dispatchEvent(
new CustomEvent('CelestoryOutput', {detail: {key: 'score', value: 42}})
);
</script>


key names the output, value carries the value. Post as often as you like: for a given key, the last value posted is the one the graph reads.


There is a second route, older but still live: if the key is written literally in the call, as above, Celestory spots it while reading the code and creates the output even without a declaration comment.


๐Ÿ”บ But as soon as the key is computed โ€” key: outputName โ€” nothing is detected any more. Always declare your outputs in the comment. It is the only method that does not depend on how you happen to write your code.



๐Ÿ Giving control back: CelestoryEnd


As long as the page is on screen, the journey is halted. To restart it, the page sends:


<script>
parent.document.dispatchEvent(new CustomEvent('CelestoryEnd'));
</script>


The flow then leaves through the block's out output. The event is idempotent: send it ten times and the graph still advances once.


Post your values before you end. Order matters: CelestoryEnd advances the flow, and the blocks downstream read whatever was posted up to that moment.


๐Ÿ”บ A page that never sends CelestoryEnd can strand the experience. The close button is your remaining way out, but it only appears if the out output is wired and the close button input is true. With neither, the player is stuck.



โš ๏ธ The trap: the listeners sit on the document


This is confirmed in the code, and it is the most common cause of failure: Celestory listens on document objects, never on a window.


Your page runs inside an isolated frame. Two listeners are attached: one on the host application's document, one on the page's own document.


<script>
// โœ… always correct โ€” the host application's document
parent.document.dispatchEvent(new CustomEvent('CelestoryEnd'));

// โœ… also correct โ€” your own page's document
document.dispatchEvent(new CustomEvent('CelestoryEnd'));

// โŒ reaches nothing, silently
window.dispatchEvent(new CustomEvent('CelestoryEnd'));
parent.dispatchEvent(new CustomEvent('CelestoryEnd'));
dispatchEvent(new CustomEvent('CelestoryEnd'));
</script>


๐Ÿ”บ Dispatching the event on a window triggers absolutely nothing, whatever the rest of your page does. No console error, no message: the page simply sits there.


๐Ÿ”บ Posting it on an element (a button, a div) does not work either, unless you make it bubble explicitly โ€” and a CustomEvent does not bubble by default. Get into the habit of parent.document; it works in every case.



๐Ÿงฉ A complete page, end to end


<!-- celestory-outputs: answer, correct -->
<!DOCTYPE html>
<html>
<body style="font-family: sans-serif; text-align: center; padding: 40px">
<h1>Hello {{firstname}}!</h1>
<p>What is 7 ร— 6?</p>
<input id="entry" type="number" />
<button id="submit">Submit</button>

<script>
document.getElementById('submit').addEventListener('click', () => {
const value = Number(document.getElementById('entry').value);

parent.document.dispatchEvent(new CustomEvent('CelestoryOutput',
{detail: {key: 'answer', value: value}}));
parent.document.dispatchEvent(new CustomEvent('CelestoryOutput',
{detail: {key: 'correct', value: value === 42}}));

parent.document.dispatchEvent(new CustomEvent('CelestoryEnd'));
});
</script>
</body>
</html>


Once you leave the editor, the block shows:


  • one input, firstname, waiting to be wired;
  • two outputs, answer (a number) and correct (a boolean);
  • the out stream output, to wire onwards โ€” into a Condition block reading correct, for instance.


The output's type follows the posted value: a number arrives as a Number, a boolean as a Boolean, everything else as Text.



๐Ÿงท The rest of the block


The close button input shows a cross in the top-right corner of the page. It is on by default and is only a safety net: clicking it does exactly what CelestoryEnd would do, with no value posted. Set it to false for a mini-game nobody should be able to walk out of โ€” provided your page knows how to end itself.



โœ… The checklist


Before you test, check these five things:


  1. You have left the editor: the points really did appear on the block.
  2. Every {{name}} is unaccented and wired.
  3. Your outputs are declared in <!-- celestory-outputs: โ€ฆ -->.
  4. All your events go to parent.document.
  5. Your page sends CelestoryEnd โ€” after posting its values.



โ†’ Next step: take the complete page above, paste it into an HTML5 block, leave the editor, wire firstname, and drop a Condition block on the correct output. That is the pattern behind every advanced use of Celestory.

Updated on: 21/09/2026

Was this article helpful?

Share your feedback

Cancel

Thank you!