:::meta description: How to publish Luccet notes: the file and its first line, opening notes from disk, static hosts and GitHub Pages, pre-rendering with luccet build, Content-Security-Policy, math and offline use, browsers, trust, performance, and what to do when a note does not render. ::: ::site-nav # Publishing {#top} ::toc A note is a static file. To publish it, put it on any web server next to `luccet.js`, or send it to someone: there is no build step, no server code and no database. This page covers what that file must contain, how it behaves from disk and from a host, how to tighten it with a Content-Security-Policy, which browsers it needs, and what to do when a note does not render. Everything here was checked in Chromium and Firefox, which the test suite drives (see [Browsers](#browsers)). Messages quoted on this page are the real ones. ## The file {#the-file} A note is one HTML file. Its first line loads `luccet.js`; everything after that line is the note. ```html ``` This is the canonical first line, and `luccet new` writes it for you (see [luccet new](tools.html#new)). | Part | Why it is there | |---|---| | `` | Standards mode. Without it the note still renders, in quirks mode, where the layout and some add-ons misbehave, and you get a `doc.quirks` error. See [The doctype](#doctype). | | `` | The file is read as UTF-8 wherever it is served from. Without it, a server that does not name a charset makes browsers guess, and non-ASCII text comes out garbled. See [Garbled characters](#charset). | | `` | Loads the engine, which reads the rest of the file as the note and renders it in place. The `src` is a path relative to the note, or a URL. | **What the tag must be.** A plain, synchronous, classic script, on one line: - no `async`, `defer` or `type="module"`: `luccet.js` must run while the browser is still reading the file, to capture the rest of it; - the file it names must be called exactly `luccet.js` (any path, any query string). The browser would run a copy under another name, such as `luccet.min.js`, but the command line and editors would not take the file for a note: keep the name. For a smaller file under the right name, load a release's minified build from `https://cdn.luccet.dev/vX.Y.Z/luccet.js`, or copy it to your site; each [GitHub Release](https://github.com/Luccet/luccet/releases) carries the same file; - the whole tag on one line, not inside a comment. The exact rule is [Note detection](tools.html#detection). Other attributes are allowed on the tag, and some configure `luccet.js` (see [The tag's attributes](#tag-attributes)). ### The doctype {#doctype} Without ``, browsers render the page in **quirks mode**, which emulates old browsers' layout bugs. `luccet.js` has a single way of rendering, in place, keeping the head. It cannot leave quirks mode without replacing the whole document, so it does not try. Instead: - the note still renders; - its layout and some add-ons misbehave in quirks mode, where the browser emulates old layout bugs; - the [panel](#local) and the console report `doc.quirks`, an error, with the fix: ```text error doc.quirks at the luccet.js line (file line 1): the file has no , so the browser renders it in quirks mode, where the layout and some add-ons misbehave (make the first line ) ``` The fix is to put `` first in the file. `luccet build` adds it to the page it writes, with a warning (see [luccet build](#build)). ### The preserved head {#head} Everything before the `luccet.js` tag is the **preserved head**: ordinary HTML that the browser reads before any script runs, and that `luccet.js` keeps as it is. It is the place for what must be in the HTML itself, for crawlers, link previews and readers' browsers: ```html Lineare Abbildungen ``` What belongs there: | Element | Use | |---|---| | `` | The page's language and direction. `lang` also picks the language of generated labels. | | ``, `<meta name="description">` … | What search engines and link previews show. A note's `:::meta` is written into the head only when `luccet.js` runs, so a crawler that runs no scripts sees only what the head holds. | | `<link rel="icon">` | The tab icon. | | `<meta name="color-scheme">` | Your colour scheme; otherwise `luccet.js` adds `light dark`. | | `<link rel="stylesheet" href="luccet.css">` | Luccet's base stylesheet as a file, instead of the copy `luccet.js` inlines. Needed under a strict [CSP](#csp). | | `<style>`, `<link rel="stylesheet">` of your own | Your styles. Luccet puts its own stylesheets before the first of yours, so yours win at equal specificity. | | `<script>window.LUCCET = {…}</script>` | The note's own vocabulary and configuration (see [window.LUCCET](schema.html#window-luccet)). Head scripts run before `luccet.js`. | What to keep out of it, or to know: - **Body content.** A `<p>` or other visible element before the tag starts the page's body, and `luccet.js` replaces the body with the note when it mounts: the element disappears. - **The charset early.** Browsers look for `<meta charset>` only in the first 1024 bytes of the file. Keep it near the top, as the canonical line does, and before any long inline script. - **One line or several.** The head may span lines. The command line and editors count file lines exactly. The panel in the browser counts the line breaks the browser keeps, and the browser drops those right after `<!doctype html>`, `<html …>` and `<head>`: with each of those on its own line, the panel's "file line" reads too low by one per line. The canonical single line avoids it. - **Scripts and the tools.** `luccet check`, `luccet build` and the language server run the head's classic scripts to find the note's vocabulary, but not modules, not `async` or `defer` scripts, and not scripts from another origin (see [Vocabulary under Node](tools.html#vocabulary)). - **When the head and `:::meta` disagree**, the head wins, with a `meta.conflict` warning. The head is what non-script readers saw. An example, where the head is `<title>From the head`: ```luccet {.live head="From the head" expect="meta.conflict"} :::meta title: From the note ::: ``` See [Metadata](elements.html#metadata) for every key. ### The tag's attributes {#tag-attributes} `luccet.js` reads these attributes from its own tag. Each can also be set in `window.LUCCET`; the tag wins. The full list, with `luccet.config`, is in the [JavaScript API](api.html#config). | Attribute | What it does | |---|---| | `data-use="a.js theme.css"` | Add-ons: scripts and stylesheets, separated by spaces, loaded in parallel and run in order before the note renders. A query string (`a.js?lang=de`) reaches the add-on as `luccet.config.use.a.lang`. A [`:::use` block](syntax-blocks.html#use) at the top of the note lists them the same way, one or more per line; the tag's load first. See [Loading](addon-api.html#loading). | | `data-css="URL"` | Luccet's base stylesheet from this URL instead of the inline copy. | | `data-nonce="VALUE"` | A CSP nonce for everything `luccet.js` adds: stylesheets, scripts, links, and the note's own scripts. See [CSP](#csp). | | `data-lang="de"` | The language of generated labels, when it differs from ``. | | `data-math="tex"` | Writes each formula as its TeX instead of MathML, for a renderer of your own ([KaTeX instead](math.html#katex)). | | `data-diagnostics` | Shows the diagnostics panel on any host, not only on your machine. `false`, `0`, `off` or `no` turn it off. | | `data-quiet` | Hides the panel everywhere. The console still reports. | `data-built` is written by `luccet build`; do not write it yourself. ### What follows the tag {#after-the-tag} Everything after the line holding the tag is the note, to the end of the file, including whatever a server or a tool appends to the file. A script tag added at the end becomes a raw HTML block of the note, and runs as a note script. ## Opening locally {#local} Double-click the note, or drag it onto a browser window. It renders from disk, from a `file:` URL, with no server. This works in Chromium and Firefox; in Safari it is not verified ([Browsers](#browsers)). - **Relative paths work.** `luccet.js`, add-ons, stylesheets and images found next to the note, or in folders below or above it, load from disk. - **Root-relative paths do not.** `/luccet.js` means the root of your disk when the note is opened from a file. Use relative paths (`../luccet.js`). - **Math works from disk**, with no network: Luccet writes it as MathML itself. An add-on loaded from a CDN, or one that fetches a library from a CDN, needs the network; see [Offline use](#offline). - **Garbled characters.** Opened from disk, both browsers read a note without `` as UTF-8 in our tests. From a server, they may not; keep the charset. **The diagnostics panel.** While you work, problems should be visible. When the note is opened from a `file:` URL, or from `localhost`, `127.0.0.1` or `[::1]`, `luccet.js` adds a panel at the bottom of the page, after the note: ```text luccet: 2 problems (1 error, 1 warning) WARNING ref.dangling line 3:5 (file line 4) no id in this note has this name, so the reference stays text if it is not a reference, write \@rank ERROR block.unclosed line 5:1 (file line 6) `:::aside` opened on line 5 is still open at line 6 add `:::` or `:::/aside` ``` - It lists every diagnostic, errors, warnings and infos, with its note line and its file line, and the hint. - It is open when there are errors, and folded otherwise. - It updates as problems appear later (an add-on that fails). - It is hidden when printing. - On any other host, readers do not see it. `data-diagnostics` on the tag shows it everywhere; `data-quiet` hides it everywhere. The browser console always gets the same list, as one collapsed group, `luccet: N problems`, whose first line gives the timings. To see a note as it will be served, run any static file server in its folder, for example `python3 -m http.server`, and open `http://localhost:8000/note.html`. The panel shows there too. ## Static hosts {#hosting} Any static host serves notes: a web server, object storage, a static-site service. Upload the notes, `luccet.js`, and the add-ons and images they use, keeping the folder layout the relative paths expect. - **No configuration** is needed: no rewrites, no headers, no server code. `.html` files must be served as `text/html`, which every host does. - **One `luccet.js`** for the whole site is the simplest: at the root, with each note pointing at it relatively (`luccet.js`, `../luccet.js`). `luccet new` writes that path for you. - **Leave the HTML alone.** A host or CDN option that rewrites HTML, such as minification, changes the note, because the note is text inside the HTML file. Anything appended to the file becomes part of the note. - **Caching.** To make readers fetch a new `luccet.js`, add a query string to the tag, `luccet.js?v=2`: the tag is still recognised. The same works for add-ons in `data-use`. - **The panel** stays hidden for readers (see [Opening locally](#local)). - **Headers.** If your host lets you set a `Content-Security-Policy`, read [CSP](#csp) first. Before uploading, check every note: ```sh luccet check site/ --quiet ``` It exits 1 when a note has an error, so it can gate a deployment (see [luccet check](tools.html#check)). ## GitHub Pages {#github-pages} GitHub Pages serves a repository's files as a static site, either from a branch or from a workflow. **From a branch.** In the repository's settings, under Pages, publish a branch (and optionally its `/docs` folder): - Add an empty file called **`.nojekyll`** at the root of what is published. Without it, GitHub runs the Jekyll site generator over the files first, which skips files and folders whose names start with `_` or `.`. With it, every file is published as it is. This repository has one. - Put **`luccet.js`** in the published folder, for example at its root, and reference it with **relative paths**. - **Project pages live under a sub-path.** A repository called `notes` is served at `https://USER.github.io/notes/`. A root-relative `/luccet.js` there means `https://USER.github.io/luccet.js`, which is not your file. Relative paths keep working at any depth, from disk and on the site. **From a workflow.** A GitHub Actions workflow can check the notes and publish a folder. GitHub then publishes exactly the folder you upload, without Jekyll: ```yaml name: pages on: push: branches: [main] permissions: contents: read pages: write id-token: write jobs: deploy: runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deploy.outputs.page_url }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 # Optional: fail the deployment when a note has an error. - run: node tools/luccet/luccet-cli.js check site/ --quiet - uses: actions/configure-pages@v5 - uses: actions/upload-pages-artifact@v3 with: path: site - id: deploy uses: actions/deploy-pages@v4 ``` Here `site/` holds the notes and `luccet.js`, and `tools/luccet/` is a copy of Luccet's repository, for the check. Choose "GitHub Actions" as the Pages source in the repository's settings. ## Pre-rendering with luccet build {#build} A note renders in the reader's browser. `luccet build` does the rendering ahead of time, and writes a page that already holds the HTML. The page still loads `luccet.js`, which adds behaviours, the panel, and copy and print handling. Its formulas are in the HTML as MathML, like the rest of the note. How the command works is on [luccet build](tools.html#build). ```sh luccet build notes/maps.html -o site/maps.html ``` **Pre-render when:** - **readers may have no JavaScript**, or `luccet.js` may be blocked: the content and its styles show anyway, math included; - **crawlers and link previews** that run no scripts should see the content, the title and the `:::meta` description, which the build writes into the head; - **long notes** should show their layout before any script runs. **Publish the note itself when:** - **its add-ons are remote** (`https://…` in `data-use`). They are not loaded under Node, so the build writes the names they declare without their schema: no labels, no numbers. The build warns about each one; - **the host sends a strict CSP** and the note has aligned table columns or `cols` grids: those come as `style` attributes in a built page (see [Built pages and CSP](#csp-built)); - you want one file that is both the source and the page. **What changes** between the note and the built page: | | The note | The built page | |---|---|---| | The content | written by `luccet.js` when the page loads | in the HTML | | `:::meta` | written into the head by `luccet.js` | in the HTML | | Without `luccet.js` | the source, run together (or the [guard](tools.html#guarded)) | the content, styled | | The note's scripts | re-created by `luccet.js`, with `document.write` and `DOMContentLoaded` intercepted and reported | real scripts, run while the page is parsed, not intercepted | | Add-ons | loaded by `luccet.js` | static ` ``` - `'self'` allows `luccet.js`, `luccet.css` and add-ons from your site. - **`luccet.js` from cdn.luccet.dev** needs it in `script-src`, and its `luccet.css` in `style-src`: `script-src 'self' https://cdn.luccet.dev; style-src 'self' https://cdn.luccet.dev`, with `luccet.css` linked from the same release's folder (`https://cdn.luccet.dev/vX.Y.Z/luccet.css`). - **Math needs nothing more.** The engine writes it as MathML, which is markup: no script, stylesheet or font is fetched for it, and it carries no `style` attributes. - **An add-on from a CDN**, or one that fetches a library from a CDN, needs that CDN in `script-src`: `script-src 'self' https://cdn.luccet.dev` for Luccet's add-ons, and `style-src 'self' https://cdn.luccet.dev` for its themes. An add-on that fetches a library (highlight.js, Mermaid, KaTeX) names the origins it needs on its page, and has its own way to use a copy from your site instead (see its page, from [Add-ons](addons.html#top)). - `luccet.css` comes from the repository, next to `luccet.js`, or from the release's folder on cdn.luccet.dev. It is the same stylesheet `luccet.js` would otherwise inline in a `