Rocket
On this page

Components#

Rocket works with custom elements in two different ways:

Use Registered Components for reusable files, server rendering, client loading, and hydration. Use Page-local Custom Elements for small one-off browser behavior that belongs to one Markdown Page.

Registered Components#

A Registered Component entry maps a tag name to:

For project components referenced from a Page, create an absolute file URL with new URL:

```js server
export const config = {
  path: '/components/button',
  metadata: { title: 'Button' },
};

import { atlasDocLayout, atlasDocComponents } from '@rocket/js/layouts/atlasDoc.js';
import { siteData } from '../siteData.js';

const acmeButtonFile = new URL('../components/AcmeButton.js', import.meta.url).href;

export const components = {
  ...atlasDocComponents,
  'acme-button': {
    file: acmeButtonFile,
    className: 'AcmeButton',
    loading: 'server',
  },
};

export const layout = pageData => atlasDocLayout(pageData, siteData);
```

# Button

<acme-button href="/setup">Read setup</acme-button>

Do not rely on a relative string such as ./components/AcmeButton.js from Page code. Component loading is performed by Rocket-owned modules and adapters, so new URL(..., import.meta.url).href keeps the file location unambiguous.

Component modules#

The module should export the class named by className:

import { LitElement, html } from 'lit';

export class AcmeButton extends LitElement {
  static properties = {
    href: { type: String },
  };

  render() {
    return html`<a href=${this.href}><slot></slot></a>`;
  }
}

Do not call customElements.define in a module used as a Registered Component. The Page owns the tag name, and Rocket defines it according to the Loading Strategy.

Loading Strategies#

Loading StrategyFirst renderBrowser codeUse when
serverRocket renders the component while rendering the PageNo component module is shipped for that tagThe HTML is the complete experience
clientThe browser imports the module before the tag becomes usefulLoaded immediatelyThe component needs browser-only state before it can render useful UI
hydrate:*Rocket renders HTML firstLoaded when the Hydration Strategy resolvesInitial HTML is useful and interaction can wait

server#

'acme-callout': {
  file: acmeCalloutFile,
  className: 'AcmeCallout',
  loading: 'server',
}

Use server for badges, callouts, static cards, menus, formatted text, and other components whose rendered HTML is enough.

The module must be safe to import in Rocket's server environment. Guard browser globals such as window, document, and localStorage.

client#

'acme-editor': {
  file: acmeEditorFile,
  className: 'AcmeEditor',
  loading: 'client',
}

Use client for browser-only widgets such as editors, maps, command palettes, authenticated controls, and components that cannot render meaningful initial HTML.

hydrate:*#

'acme-tabs': {
  file: acmeTabsFile,
  className: 'AcmeTabs',
  loading: 'hydrate:onVisible',
}

Use hydrate:* when Rocket should render useful HTML first and make it interactive later.

Supported Hydration Strategies include:

Hydration conditions can be combined:

loading: 'hydrate:onVisible||onClick';
loading: "hydrate:onVisible&&onMedia('(min-width: 1024px)')";

Component Hydration requires stable server HTML. The component should tolerate being rendered on the server and later upgraded in the browser with matching initial state.

Rocket-owned components#

Rocket ships rocket-icon as a built-in Registered Component for trusted SVG icons. See Rocket Icon for authoring, loading behavior, Icon Libraries, and generated asset output.

Page-local Custom Elements#

For a small one-off element in a Markdown Page, define the tag in js client:

```js client
class InlineCounter extends HTMLElement {
  connectedCallback() {
    let count = 0;
    this.innerHTML = `<button type="button">Count: 0</button>`;
    this.querySelector('button').addEventListener('click', event => {
      count += 1;
      event.currentTarget.textContent = `Count: ${count}`;
    });
  }
}

customElements.define('inline-counter', InlineCounter);
```

<inline-counter></inline-counter>

Rocket validates Page-local ownership from literal customElements.define('tag-name', ClassName) calls in that Page's js client code. If the tag name is computed, Rocket cannot prove ownership.

Markdown validation#

Markdown Pages are strict about authored custom element tags. Any tag name containing a hyphen must be owned by the Page:

A tag cannot use both ownership models at the same time. Tags that appear only inside js demo blocks do not satisfy ownership for the parent Markdown Page.

JavaScript Pages#

JavaScript Pages use the same explicit components export when they render Registered Components:

import { html } from 'lit';
import { ssrRender } from '@rocket/js/ssr.js';
import { atlasDocLayout, atlasDocComponents } from '@rocket/js/layouts/atlasDoc.js';
import { siteData } from '../siteData.js';

const acmeTabsFile = new URL('../components/AcmeTabs.js', import.meta.url).href;

export const components = {
  ...atlasDocComponents,
  'acme-tabs': {
    file: acmeTabsFile,
    className: 'AcmeTabs',
    loading: 'hydrate:onVisible',
  },
};

export default async (_request, { pageData }) => {
  pageData.content = html`
    <h1>Tabs</h1>
    <acme-tabs></acme-tabs>
  `;

  return new Response(await ssrRender(atlasDocLayout(pageData, siteData)), {
    headers: {
      'Content-Type': 'text/html; charset=utf-8',
    },
  });
};

export const config = {
  path: '/components/tabs',
};

This example can stay static because the path is concrete and the response can be rendered once during the build. Add render: 'server' when the JavaScript Page needs request-time data, adapter context, or route parameters.

Debugging#

For a decision guide, see Component Loading.

For Markdown-specific ownership rules and code fence behavior, see Markdown Authoring.