Cheatsheet

Start

create-rebask-app --style css skips Tailwind.

pnpm create rebask-app

vite.config.ts

import rsc from "@vitejs/plugin-rsc"
import react from "@vitejs/plugin-react"
import { rebask } from "@rebask/core/vite"

export default defineConfig({
  plugins: [rsc(), react(), rebask()],
})

Routing

app/

app/
  layout.tsx            wraps below
  page.tsx              /
  about/page.tsx        /about
  blog/[slug]/page.tsx  /blog/:slug
  api/hi/route.ts       /api/hi

(group) Groups files, not part of the URL.

Project files

Pages and layoutsserver

page.tsx Default export, receives no props.

layout.tsx Receives children. The root one renders <html>.

prerenderParams() allowExtraParams: false 404s other paths.

export function prerenderParams() {
  return {
    params: [{ id: "1" }, { id: "2" }],
    allowExtraParams: false,
  }
}

Route handlersserver

route.ts

export async function GET(
  req: Request,
  ctx: { params: { id: string } },
) {
  return Response.json(ctx.params)
}

Directives

"use client" State, effects, and events. Server is the default.

"use cache" Caches the result. Stale after an hour, then refreshed in the background.

Requestserver

@rebask/core/request · @rebask/core/cookies

routeParams() Read under <Suspense> to keep the rest in the shell.

cookies()

const store = await cookies()
store.get("id")
store.set("id", id, { maxAge: 3600 })
store.delete("id")

Server Functions

<form action> Works without JavaScript. Receives FormData.

<form action={createPost}>
  <input name="title" />
</form>

useActionStateclient The action gets (prevState, formData).

const [state, action, pending] =
  useActionState(save, { error: null })

After a mutation Client data isn't refreshed on its own.

await revalidateTag("posts") // server
await router.revalidate()    // client

Navigations

@rebask/core/components/link · @rebask/core/router

<Link>

<Link href="/a">A</Link>
<Link href="/b" prefetch="none" />
<Link href="/c" scroll={false} />

Redirectserver Render a meta refresh when the target depends on page data. Full document navigation.

if (!post) return (
  <meta httpEquiv="refresh" content="0;url=/blog" />
)

useRouter()client

const router = useRouter()
router.push("/a", { scroll: false })
router.replace("/login")
await router.prefetch("/checkout")

<Suspense> There is no implicit boundary. On a soft navigation, the nearest boundary catches the loading page segment, and a root-only fallback replaces everything it wraps. Wrap the dynamic parts of the page with Suspense, and wrap children in the closest layout to catch the page segment.

Client cache

@rebask/core/router · @rebask/core/components/cache-hints · @rebask/core/components/revalidate-segment · @rebask/core/components/route-hints

router.revalidate()client Refetch while showing stale data.

await router.revalidate()
await router.revalidate("/posts", {
  evict: true, // show the fallback
  immediate: true, // even if hidden
})

<CacheHints>server Seconds this browser keeps it fresh, minimum 2. mustRevalidate disables SWR.

<CacheHints stale={60} tags={["a"]} />

Server cacheserver

@rebask/core/cache

"use cache"

async function getPost(id: string) {
  "use cache"
  cacheLife({ revalidate: 60 })
  cacheTag("posts", `post:${id}`)
  return db.posts.find(id)
}

durableCache(fn, deps) Survives deploys. JSON only, list other inputs in deps.

const getRates = durableCache(
  async (id: string) => getRate(id),
  [API_URL],
)

fetch() Identical GETs run once per request. A signal opts out.

Errors

@rebask/core/components/errors · @rebask/core/components/error-boundary

<NotFound>server Return it when data is missing. Sets a 404.

if (!post) {
  return <NotFound>Gone</NotFound>
}

<ServerError status={503}>server Your error UI with an error status. Not cached. Handle errors with try/catch and return it from catch.

Metadata

@rebask/core/components/head

<Head> Dedupes across layouts.

Images

@rebask/core/vite/image · @rebask/core/image · @rebask/core/components/image

rebaskImage()

// vite.config.ts
plugins: [/* … */, rebaskImage()]

// rebask.image.ts
export default (req: Request) =>
  handleImage(req, sharp)

<Image> Also placeholder="blur", quality, loader.

<Image src="/a.jpg" alt="" preload
  width={800} height={600} />
<Image src={url} alt="" fill />

Proxyserver

@rebask/core/proxy

proxy.ts Return a Response to end, nothing to continue.

export default {
  "/admin/*": (req) => {
    if (!cookies(req).has("sid"))
      return Response.redirect(login)
  },
  "/old/:slug": (_, { slug }) =>
    rewrite(`/blog/${slug}`),
} satisfies ProxyRoutes

:slug * params.slug, params["*"] for one or more segments.

Config

@rebask/core/vite · @rebask/core

rebask() Defaults.

rebask({
  prerender: true,
  dedupeFetch: true,
  fullHtmlForBots: false,
  markdownUrls: true,
  proxyTimeout: 30_000,
  shellPrerenderTimeout: 30_000,
  clientTraceMetadata: [],
})

rebask.config.ts

export default {
  cacheHandler: new RedisCache(),
  serverFunctions: {
    allowedOrigins: ["*.example.com"],
    bodySizeLimit: 1024 * 1024,
  },
} satisfies RebaskConfig