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

Guides

Client Navigation & Images

Every plain <a> tag on an X site already navigates client-side, with no router setup required. This page covers that behavior, the <Link> convenience component, the built-in remote image proxy, and the two error surfaces (dev overlay, 404 page).

Client-side navigation, by default

Every rendered page inlines a small navigation script (CLIENT_NAV_SCRIPT). It intercepts clicks on same-origin <a href> elements, fetches the destination page, and swaps the page content in place instead of doing a full browser navigation, so you get SPA-style transitions without adding a router or wrapping links in anything. It also prefetches on hover/focus and handles back/forward via popstate.

just works
// No import needed — this already does client-side nav + hover prefetch:<a href="/docs/routing">Routing</a>

Opt individual links out with data attributes:

opt out
<a href="/legacy" data-no-nav>Full page load</a><a href="/heavy-page" data-no-prefetch>No hover prefetch, still client nav</a>

Links are skipped automatically if they cross origins, open a new tab (target), carry a download attribute, or use a #/mailto:/tel: scheme. Those always behave like normal anchors.

The <Link> component

<Link> is a typed wrapper around the same behavior above. Use it when you want the opt-out props to be type-checked instead of stringly-typed data attributes:

Link

Remote image proxy

createImageProxyHandler mounts a GET /_x/image route that fetches an allow-listed remote image server-side and streams it back from your own origin. The browser never makes a cross-origin image request, so a strict img-src 'self' CSP (see Security) still works even with remote images. This page's own Stardance badge is proxied through it right now.

createApp.ts / x.config.ts wiring
import { createImageProxyHandler } from "@thexjs/core";const imageProxy = createImageProxyHandler({  remoteHosts: ["stardance.hackclub.com"], // required allow-list — empty means requests are rejected});
usage
<img src={`/_x/image?url=${encodeURIComponent("https://stardance.hackclub.com/logo.png")}`} /> >

It's a proxy, not an optimizer. No resizing or format conversion happens. Only hosts in remoteHosts are ever fetched (this is what prevents the route from becoming an open SSRF relay), only a fixed set of image content types are allowed through, and successful responses are served with a one-day, immutable Cache-Control header.

The <Image> component & srcSet

While the proxy streams a single remote URL, <Image> handles responsive images end to end. Remote sources are routed through /_x/image automatically and get a srcSet generated for them; local sources pass through untouched. Either way you avoid layout shift and keep img-src 'self'.

Image (remote source)
import { Image } from "@thexjs/core";<Image  src="https://cdn.example.com/hero.jpg"  width={1200}  height={630}  sizes="(min-width: 768px) 50vw, 100vw"  alt="Product hero"/> >

For a remote src whose host is in images.remoteHosts, the generated srcSet re-serves the image through the proxy at each breakpoint in SRCSET_WIDTHS. A host that isn't allow-listed 403s at request time, and warns in dev:

SRCSET_WIDTHS
export const SRCSET_WIDTHS = [320, 640, 750, 828, 1080, 1200, 1920, 2048, 3840];// remote & allow-listed -> "…/_x/image?url=…&w=320 320w, …&w=640 640w, …&w=750 750w, …"// local src            -> plain <img src>, no srcSet (no resize pipeline)

Build that string manually with buildSrcSet(src, opts) when you need custom behavior. Beyond responsive sizing, priority sets fetchPriority="high" on LCP images (everything else lazy-loads), fill absolutely positions the image in its parent (nice for hero/card layouts, exclusive with width/height), and placeholder="blur" with blurDataURL shows a tiny base64 blur that fades out on load. It is CSS-only; no JavaScript island is required.

Dev error overlay

When a loader, page, or API route throws during x dev, renderErrorOverlay renders a full-screen overlay with the error message and stack trace, instead of a bare 500 response. It's dev-only: production builds never ship the overlay, they return a plain error response.

404 handling

Drop a src/pages/_404.tsx to customize the site-wide not-found page. If you don't provide one, X falls back to DefaultNotFound, a minimal built-in page, so every project has a sane 404 without extra setup.

src/pages/_404.tsx
export default function NotFound() {  return <h1>Nothing here.</h1>;}