newX 1.3: islands to disk, server-mode islands, and an image proxy

Tutorial

Hello World

Build a page, add an interactive counter island, run the dev server, and ship a production build. This tutorial takes about five minutes and requires only Bun.

1. Scaffold the project

Create a new X project with the scaffolder. Accept the defaults at the feature prompt (press enter with nothing selected) for a blank project:

terminal
$bun create thexjs-app@latest hello-world
? Select features (space to toggle, enter to confirm) › done
✓ created hello-world/
✓ installed dependencies
$ cd hello-world
$ x dev
[x] dev server running at http://localhost:3000

Open http://localhost:3000 in your browser. You should see a blank page with "Hello x!" — that is the default home page from src/pages/index.tsx.

2. Edit the home page

Open src/pages/index.tsx and replace its content with a page that has a title and a placeholder for an interactive counter:

src/pages/index.tsx
import { Island } from "@thexjs/core";import { Counter } from "../components/counter";export const islands = { Counter };export default function Home() {  return (    <div className="mx-auto max-w-2xl py-20 text-center">      <h1 className="text-4xl font-bold">Hello from x!</h1>      <p className="mt-3 text-muted-foreground">        This page is fully server-rendered. The counter below is an island:        it hydrates in the browser while everything else ships zero JS.      </p>      <Island name="Counter" client="load">        <Counter /> >      </Island>    </div>  );}

The page imports Island from @thexjs/core, registers Counter via export const islands, and renders it inside an <Island> wrapper with client="load" (hydrate immediately on page load). The dev server hot-reloads the page as soon as you save the file.

3. Create the island component

Create src/components/counter.tsx with a simple interactive counter that uses useState:

src/components/counter.tsx
import { useState } from "react";export function Counter() {  const [count, setCount] = useState(0);  return (    <div className="mt-8">      <p className="text-lg">Count: {count}</p>      <div className="mt-4 flex justify-center gap-3">        <button          onClick={() => setCount((c) => c - 1)}          className="rounded-lg border border-border bg-card px-4 py-2 text-sm font-medium hover:bg-muted"        >          -1        </button>        <button          onClick={() => setCount((c) => c + 1)}          className="rounded-lg border border-border bg-card px-4 py-2 text-sm font-medium hover:bg-muted"        >          +1        </button>      </div>    </div>  );}

Because <Counter> is registered as an island on the page, only this component's JavaScript ships to the browser. The heading, paragraph, and wrapper div are just static HTML.

4. See it hydrate

Save both files. The dev server picks up the changes and reloads the page automatically. Open your browser's developer tools and check the Network tab — you should see a small JavaScript chunk load for the island:

terminal · x dev output
$x dev
[x] resolving routes...
[x] ── / (index.tsx)
[x] compiling island bundles...
[x] ── Counter (client=load)
[x] dev server running at http://localhost:3000

Click the +1 and -1 buttons — they work without a full page reload because the island hydrated its React state in-place. The rest of the page (the heading, the paragraph) was never re-rendered or hydrated.

5. Production build

Stop the dev server (Ctrl+C) and run a production build:

terminal · x build
$x build
[x] resolving routes...
[x] building static pages...
[x] ✓ / (index.tsx)
[x] building island bundles...
[x] ✓ / Counter
[x] building server bundle...
[x] build complete in 0.9s -> .x

The .x/ directory now contains:

  • .x/client/index.html — the prerendered HTML with the counter's server-rendered markup embedded
  • .x/client/_islands/ — the Counter hydration chunk (a few hundred bytes)

Because the page has no mode export, it defaults to server mode. If you export mode = "static", the page prerenders at build time and can deploy to any static host — the island still hydrates the same way.