create-rebask-app --style css skips Tailwind.
pnpm create rebask-appcreate-rebask-app --style css skips Tailwind.
pnpm create rebask-appimport rsc from "@vitejs/plugin-rsc"
import react from "@vitejs/plugin-react"
import { rebask } from "@rebask/core/vite"
export default defineConfig({
plugins: [rsc(), react(), rebask()],
})vite · vite build · vite preview Dev, build, serve.
app/
layout.tsx wraps below
page.tsx /
about/page.tsx /about
blog/[slug]/page.tsx /blog/:slug
api/hi/route.ts /api/hi[id] id: string
[...slug] slug: string[]
[[...slug]] slug?: string[]
(group) Groups files, not part of the URL.
rebask.config.ts Runtime config.
proxy.ts Runs before any route.
instrumentation.ts register() runs on boot.
rebask.image.ts Image optimizer.
public/ Static files.
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,
}
}export const notFound = true On a catch-all page, the 404 for unmatched URLs.
markdown.md / markdown.ts Markdown for Accept: text/markdown and the route's .md URL.
export async function GET(
req: Request,
ctx: { params: { id: string } },
) {
return Response.json(ctx.params)
}GET POST PUT PATCH DELETE Named exports.
HEAD OPTIONS Automatic. Missing methods get a 405.
Cache-Control Responses are only cached if you send it.
"use client" State, effects, and events. Server is the default.
"use server" Exports become functions the client can call.
"use cache" Caches the result. Stale after an hour, then refreshed in the background.
@rebask/core/request · @rebask/core/cookies
headers() searchParam() routeParams() Not available in "use cache".
const h = await headers()
const q = await searchParam("q")
const { id } = await routeParams<P>()searchParam() null, a string, or an array when repeated.
routeParams() Read under <Suspense> to keep the rest in the shell.
const store = await cookies()
store.get("id")
store.set("id", id, { maxAge: 3600 })
store.delete("id")cookies(request) cookies(response) For route handlers and the proxy. Request is read only.
<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 }).bind(null, id) Bound args first, FormData last.
Inline "use server" Define it in a Server Component, pass it as a prop.
After a mutation Client data isn't refreshed on its own.
await revalidateTag("posts") // server
await router.revalidate() // client@rebask/core/components/link · @rebask/core/router
<Link href="/a">A</Link>
<Link href="/b" prefetch="none" />
<Link href="/c" scroll={false} />prefetch="intent" Default. Shell in view, full on hover. Like viewport on mobile.
prefetch="viewport" Full payload when visible.
prefetch="none" Shell in view, full on click. Nothing prefetches in dev.
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" />
)router.replace()client Redirect without a full reload or history entry.
history.pushState() replaceState()client Update the URL and hooks without navigating.
useRouter()client
const router = useRouter()
router.push("/a", { scroll: false })
router.replace("/login")
await router.prefetch("/checkout")usePathname() useParams()client Read under <Suspense>.
<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.
@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"]} />router.revalidateTag()client Refetch by client tag. router.setCacheHints sets them.
<RevalidateSegment /> Refetch on focus and reconnect. interval polls.
<RouteHints prediction={false} />server Stop reusing /blog/[slug] for /blog/2.
@rebask/core/cache
async function getPost(id: string) {
"use cache"
cacheLife({ revalidate: 60 })
cacheTag("posts", `post:${id}`)
return db.posts.find(id)
}cacheLife({ revalidate, expire }) Seconds, shortest wins. Stale after revalidate, gone after expire (a year). In a page, sets the page lifetime.
cacheTag(...tags) Tags also flow up to the page.
revalidateTag(tag) Expires every server entry with the tag.
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.
@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.
<ErrorBoundary fallback> Client component. Catches server errors too, but the fallback only renders in the browser.
@rebask/core/components/head
<title> <meta> Render them anywhere. There is no metadata export.
<Head> Dedupes across layouts.
@rebask/core/vite/image · @rebask/core/image · @rebask/core/components/image
// 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 />@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.
rewrite("https://…") Proxies to another origin. 502 on failure.
@rebask/core/vite · @rebask/core
rebask() Defaults.
rebask({
prerender: true,
dedupeFetch: true,
fullHtmlForBots: false,
markdownUrls: true,
proxyTimeout: 30_000,
shellPrerenderTimeout: 30_000,
clientTraceMetadata: [],
})export default {
cacheHandler: new RedisCache(),
serverFunctions: {
allowedOrigins: ["*.example.com"],
bodySizeLimit: 1024 * 1024,
},
} satisfies RebaskConfig