Language
Build-time code
${ }$: Node.js that runs while the page is built.
What it is
${ }$ is JavaScript that runs in Node.js while the page is built. Only its result goes into the HTML; the browser never sees the code:
${ const year = 2026; }$
[footer]© ${ year }$ My site[end]<footer>© 2026 My site</footer>
How it works
It runs in Node.js, while building
The code runs once per build, before any visitor sees the page. Its console.log prints in your terminal, not in the browser:
${ console.log("Building the home page"); }$It runs again for each use of a component
Every use gets its own props, so the same code can write different HTML:
[Card = title: "First" !] # Card.arcm runs with title "First"
[Card = title: "Second" !] # and again with title "Second"Rules
Break one of these and your code stops the build or doesn't do what you expect.
1. There is no browser
document, window and the page's elements don't exist yet. Use runtime code for those:
# ✗ ReferenceError: document is not defined
[p]${ document.title }$[end]
# ✓ runs in the browser
[p]runtime ${ document.title }$[end]2. The last expression is the output
For several statements, end with return. It must be the last statement, not inside an if:
# ✗ return is only allowed as the last statement of ${ }$
[p]${ if (ready) { return "Ready"; } }$[end]
# ✓ right
[p]${ ready ? "Ready" : "Not yet" }$[end]3. Output text, numbers, { raw }, arrays, or nothing
Other objects stop the build. Output a field of the object instead:
${ const user = { name: "Adam" }; }$
# ✗ can't render [object Object]; return text, { raw }, or an array
[p]${ user }$[end]
# ✓ right
[p]${ user.name }$[end]4. Variables live where you make them
At the top of the file, they work everywhere below. Inside an element, they end when it closes:
${ const site = "My site"; }$
[header]${ site }$[end] # ✓ top level: works below
[div]${ const inner = 1; }$[end]
[p]${ inner }$[end] # ✗ ReferenceError: inner is not defined5. Components use [import], not JavaScript import
# ✗ Unknown file extension ".arcm"
${ import Card from "./Card.arcm"; }$
# ✓ right
[import = Card: "./Card.arcm" !]6. ArcMoon never installs packages
Install them first, or the import fails:
npm install dayjs${ import dayjs from "dayjs"; }$
[p]Built ${ dayjs().format("D MMM YYYY") }$[end]7. Read a component's props at the top of its file
Inside an element, ArcMoon.props() returns that element's props instead:
${ const { title } = ArcMoon.props(); }$ # ✓ the component's props
[h2]${ title }$[end]
[h2]${ ArcMoon.props().title }$[end] # ✗ the [h2]'s props⚠ Card.arcm:3:7 ArcMoon.props() inside [h2] returns the [h2]'s props, which have no "title"; for the component's props, call it at the top of the file8. Code has a time limit
Each page gets 5 seconds, its components included. Slower code is stopped; raise timeout in arcmoon.config.js if a page needs more:
export default { timeout: 10000 };What it outputs
The last expression is the output. Try it: change the values and press Run:
[p]${ 6 * 7 }$[end]
[p]${ "<b>escaped</b>" }$[end]
[p]${ { raw: "<b>written as HTML</b>" } }$[end]
[p]${ ["one", " two", " three"] }$[end]
[p]Nothing here: ${ null }$${ false }$[end]
- Text and numbers are escaped, so
<b>shows as text. { raw: "…" }writes HTML as it is.- An array writes each item.
null,undefined,trueandfalsewrite nothing. Other objects stop the build.
For more than one line, end with return:
[p]${
const items = ["tea", "cake"];
return items.join(" and ");
}$[end]<p>tea and cake</p>
Variables
A variable made at the top of the file can be used anywhere below it:
${ const name = "Adam"; }$
[h1]Hi, ${ name }$[end]
[p]${ name.toUpperCase() }$[end]<h1>Hi, Adam</h1>
<p>ADAM</p>
One made inside an element only lives until that element closes:
[div]${ const inner = 1; }$[end]
[p]${ inner }$[end]
✗ ReferenceError: inner is not definedImports and files
Import .js and .json files, node: modules and npm packages. Relative paths start from the .arcm file:
{ "name": "My site", "author": "Adam" }export const money = (n) => `$${n.toFixed(2)}`;${
import { name, author } from "./site.json";
import { money } from "./format.js";
import { readFileSync } from "node:fs";
const about = readFileSync(new URL("./about.txt", import.meta.url), "utf8").trim();
}$
[h1]${ name }$[end]
[p]${ about }$[end]
[p]By ${ author }$, from ${ money(5) }$ a month.[end]<h1>My site</h1>
<p>ArcMoon builds pages from .arcm files.</p>
<p>By Adam, from $5.00 a month.</p>
- JSON works with named imports:
import { name } from "./site.json". - To read a file next to the page, use
new URL("./file", import.meta.url). - Components are imported with
[import], not with JavaScriptimport. - ArcMoon never installs packages: run
npm installyourself.
Waiting for data
await works at the top of ${ }$, so the page can wait for files or an API:
${
const res = await fetch("https://api.github.com/repos/Adam-Elmi/ArcMoon");
const repo = await res.json();
}$
[p]${ repo.stargazers_count }$ stars[end]The ArcMoon object
ArcMoon.version: the ArcMoon version, e.g.1.0.0.ArcMoon.props(): the props a component was given. See Components.
For everything else, use plain Node.js: fetch, process.env, node:fs.
Tips
Your code works either way, but these make it better.
Read files relative to the page
A plain path is read from the folder you run arcm build in, so it breaks when you build from elsewhere. import.meta.url always starts at the .arcm file:
${ import { readFileSync } from "node:fs"; }$
# ✗ depends on where you run arcm build
[p]${ readFileSync("note.txt", "utf8") }$[end]
# ✓ always next to this file
[p]${ readFileSync(new URL("./note.txt", import.meta.url), "utf8") }$[end]Keep secrets at build time
Use API keys in ${ }$ freely: the code never reaches the browser. But anything you output or export is in the page for every visitor:
${
const key = process.env.API_KEY;
const res = await fetch(`https://api.example.com/stats?key=${key}`);
const stats = await res.json();
}$
[p]${ stats.visits }$ visits[end] # ✓ only the result is in the pageUse { raw } only for HTML you trust
{ raw } writes HTML as it is, scripts included. For text from users or other sites, output it plainly so it's escaped.
Prefer build-time code for values that never change
${ }$ writes the value into the HTML: no script, and it shows before any JavaScript loads. Use runtime code only for what changes in the browser.
Errors and limits
An error stops the build and points to the spot in the .arcm file:
✗ ReferenceError: undefinedThing is not defined
page.arcm:1:7Each page's code gets 5 seconds; change it with timeout in arcmoon.config.js. Slower code is stopped:
✗ page.arcm ${ }$ code took longer than 5000 ms and was stopped