@timber-js/app 0.2.0-alpha.196 → 0.2.0-alpha.198
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-CWYtq6ii.js → actions-BS-m5SLv.js} +3 -3
- package/dist/_chunks/{actions-CWYtq6ii.js.map → actions-BS-m5SLv.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
- package/dist/_chunks/{cache-api-CQeYzA5g.js → cache-api-DqzgTEqk.js} +4 -49
- package/dist/_chunks/cache-api-DqzgTEqk.js.map +1 -0
- package/dist/_chunks/{chains-h7EO-u3n.js → chains-CZG7E5zg.js} +2 -2
- package/dist/_chunks/{chains-h7EO-u3n.js.map → chains-CZG7E5zg.js.map} +1 -1
- package/dist/_chunks/{cli-check-BVthpfLS.js → cli-check-dVDi1GQz.js} +3 -3
- package/dist/_chunks/{cli-check-BVthpfLS.js.map → cli-check-dVDi1GQz.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js → cli-schema-sync-DTy_-Msq.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js.map → cli-schema-sync-DTy_-Msq.js.map} +1 -1
- package/dist/_chunks/{cloudflare-BKJC3SC_.js → cloudflare-BFb__LYG.js} +2 -2
- package/dist/_chunks/{cloudflare-BKJC3SC_.js.map → cloudflare-BFb__LYG.js.map} +1 -1
- package/dist/_chunks/{convention-lint-DO10_pVl.js → convention-lint-Ph6luW4c.js} +4 -2
- package/dist/_chunks/convention-lint-Ph6luW4c.js.map +1 -0
- package/dist/_chunks/{error-boundary-D-lkwyaD.js → error-boundary-BvRCCmbN.js} +3 -3
- package/dist/_chunks/{error-boundary-D-lkwyaD.js.map → error-boundary-BvRCCmbN.js.map} +1 -1
- package/dist/_chunks/{href-validation-CMc5JRls.js → href-validation-BIrxavIy.js} +74 -2
- package/dist/_chunks/href-validation-BIrxavIy.js.map +1 -0
- package/dist/_chunks/{live-graph-Bx4HodF1.js → live-graph-BXDsdzBv.js} +3 -3
- package/dist/_chunks/{live-graph-Bx4HodF1.js.map → live-graph-BXDsdzBv.js.map} +1 -1
- package/dist/_chunks/{logger-pumCm3Il.js → logger-DDirEsn7.js} +3 -4
- package/dist/_chunks/{logger-pumCm3Il.js.map → logger-DDirEsn7.js.map} +1 -1
- package/dist/_chunks/navigation-root-B00jjGd5.js +233 -0
- package/dist/_chunks/navigation-root-B00jjGd5.js.map +1 -0
- package/dist/_chunks/{segment-context-CjOlyB8Y.js → param-value-C8TNYchQ.js} +2 -33
- package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
- package/dist/_chunks/{poison-scan-BAxfTT5L.js → poison-scan-BoDLgbix.js} +2 -2
- package/dist/_chunks/{poison-scan-BAxfTT5L.js.map → poison-scan-BoDLgbix.js.map} +1 -1
- package/dist/_chunks/{router-ref-BzqbPwYC.js → router-ref-8gr8qsxN.js} +2 -2
- package/dist/_chunks/{router-ref-BzqbPwYC.js.map → router-ref-8gr8qsxN.js.map} +1 -1
- package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js → rsc-cache-key-ClUiXQnK.js} +2 -2
- package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js.map → rsc-cache-key-ClUiXQnK.js.map} +1 -1
- package/dist/_chunks/{scanner-BRIOmHE2.js → scanner-tdFPvDYi.js} +174 -7
- package/dist/_chunks/scanner-tdFPvDYi.js.map +1 -0
- package/dist/_chunks/segment-context-D9_89u34.js +34 -0
- package/dist/_chunks/segment-context-D9_89u34.js.map +1 -0
- package/dist/_chunks/singleflight-2lUWfcAk.js +54 -0
- package/dist/_chunks/singleflight-2lUWfcAk.js.map +1 -0
- package/dist/_chunks/{ssr-data-Ya2HJPFp.js → ssr-data-BQGhTPAK.js} +2 -17
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +1 -0
- package/dist/_chunks/{walkers-BU6z9xRV.js → walkers-DNX05dC0.js} +2 -2
- package/dist/_chunks/{walkers-BU6z9xRV.js.map → walkers-DNX05dC0.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.d.ts +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +1 -1
- package/dist/cache/singleflight.d.ts +2 -0
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/hydrate.d.ts +21 -15
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +4 -3
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +17 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/global-context.d.ts +15 -0
- package/dist/client/global-context.d.ts.map +1 -0
- package/dist/client/index.js +138 -35
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +0 -1
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +206 -55
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/location-search.d.ts +12 -0
- package/dist/client/location-search.d.ts.map +1 -0
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +18 -0
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +13 -11
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +47 -108
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +136 -0
- package/dist/client/navigation-transition.d.ts.map +1 -0
- package/dist/client/nuqs-adapter.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +4 -5
- package/dist/client/params-context.d.ts.map +1 -1
- package/dist/client/react-root.d.ts +44 -0
- package/dist/client/react-root.d.ts.map +1 -0
- package/dist/client/router-pipeline.d.ts +2 -2
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +12 -2
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +39 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-outlet.d.ts +25 -14
- package/dist/client/segment-outlet.d.ts.map +1 -1
- package/dist/client/segment-update-context.d.ts +3 -9
- package/dist/client/segment-update-context.d.ts.map +1 -1
- package/dist/client/slot-content-cache-context.d.ts +35 -0
- package/dist/client/slot-content-cache-context.d.ts.map +1 -0
- package/dist/client/ssr-data.d.ts +8 -2
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/state.d.ts +0 -15
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/use-pathname.d.ts +13 -11
- package/dist/client/use-pathname.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +13 -13
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/client/use-segment-params.d.ts +18 -68
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/config-types.d.ts +17 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +15 -10
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -43
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +2 -2
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception-overlap.d.ts +35 -0
- package/dist/routing/interception-overlap.d.ts.map +1 -0
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +3 -1
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +6 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/csp-nonce.d.ts +45 -0
- package/dist/server/csp-nonce.d.ts.map +1 -0
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/flight-scripts.d.ts +5 -2
- package/dist/server/flight-scripts.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +17 -2
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +3 -2
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +86 -37
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-render.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +3 -17
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/nuqs-ssr-provider.d.ts +7 -3
- package/dist/server/nuqs-ssr-provider.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/prebuilt/key-discipline.d.ts +32 -3
- package/dist/server/prebuilt/key-discipline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +4 -3
- package/dist/server/render-utils.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +22 -2
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +5 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +59 -27
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/types.d.ts +10 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/shims/navigation-rsc.d.ts +21 -0
- package/dist/shims/navigation-rsc.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +1 -0
- package/docs/api/35-api-typescript.mdx +4 -83
- package/docs/learn/03-fetching-data.mdx +1 -1
- package/docs/learn/{03b-access-control.mdx → 04-access-control.mdx} +2 -17
- package/docs/learn/05-the-flush-point.mdx +175 -0
- package/docs/learn/{05-typed-params.mdx → 06-typed-params.mdx} +1 -1
- package/docs/learn/07-typed-routes.mdx +25 -49
- package/docs/learn/{08-streaming.mdx → 09-streaming.mdx} +1 -7
- package/docs/learn/{10-middleware.mdx → 11-middleware.mdx} +1 -0
- package/package.json +3 -3
- package/src/adapters/nitro.ts +7 -7
- package/src/cache/singleflight.ts +5 -0
- package/src/client/browser-entry/hydrate.ts +54 -104
- package/src/client/browser-entry/index.ts +16 -6
- package/src/client/browser-entry/post-hydration.ts +3 -2
- package/src/client/browser-entry/router-init.ts +84 -33
- package/src/client/global-context.ts +31 -0
- package/src/client/internal.ts +1 -2
- package/src/client/link.tsx +18 -18
- package/src/client/location-search.ts +15 -0
- package/src/client/navigation-api.ts +4 -2
- package/src/client/navigation-commit.ts +48 -2
- package/src/client/navigation-context.ts +25 -37
- package/src/client/navigation-root.tsx +55 -411
- package/src/client/navigation-transition.ts +278 -0
- package/src/client/nuqs-adapter.tsx +4 -5
- package/src/client/params-context.ts +13 -18
- package/src/client/react-root.ts +72 -0
- package/src/client/router-lifecycle.ts +1 -1
- package/src/client/router-pipeline.ts +96 -22
- package/src/client/router-types.ts +12 -2
- package/src/client/router.ts +48 -36
- package/src/client/segment-cache.ts +70 -2
- package/src/client/segment-context.ts +7 -4
- package/src/client/segment-outlet.tsx +41 -86
- package/src/client/segment-update-context.ts +7 -26
- package/src/client/slot-content-cache-context.ts +43 -0
- package/src/client/ssr-data.ts +8 -2
- package/src/client/state.ts +0 -26
- package/src/client/use-pathname.ts +21 -31
- package/src/client/use-search-params.ts +31 -29
- package/src/client/use-segment-params.ts +27 -126
- package/src/config-types.ts +17 -0
- package/src/config-validation.ts +17 -0
- package/src/dev-tools/holding-server.ts +23 -12
- package/src/index.ts +26 -11
- package/src/plugins/dev-server.ts +9 -12
- package/src/plugins/entries.ts +3 -0
- package/src/plugins/shims.ts +8 -7
- package/src/plugins/static-build.ts +9 -5
- package/src/react-canary.d.ts +2 -0
- package/src/routing/codegen-write.ts +2 -0
- package/src/routing/interception-overlap.ts +141 -0
- package/src/routing/interception.ts +118 -5
- package/src/rsc-runtime/ssr.ts +3 -2
- package/src/server/als-registry.ts +6 -0
- package/src/server/csp-nonce.ts +70 -0
- package/src/server/default-status-page.ts +1 -0
- package/src/server/deny-renderer.ts +7 -3
- package/src/server/flight-scripts.ts +9 -4
- package/src/server/html-injector-core.ts +26 -9
- package/src/server/html-injectors.ts +8 -8
- package/src/server/metadata-render.ts +26 -4
- package/src/server/node-stream-transforms.ts +7 -20
- package/src/server/nuqs-ssr-provider.tsx +8 -7
- package/src/server/pipeline-phases.ts +5 -0
- package/src/server/prebuilt/key-discipline.ts +82 -13
- package/src/server/prebuilt-runtime.ts +2 -2
- package/src/server/primitives.ts +4 -4
- package/src/server/render-utils.ts +8 -4
- package/src/server/rsc-entry/action-middleware-runner.ts +7 -0
- package/src/server/rsc-entry/error-renderer.ts +5 -2
- package/src/server/rsc-entry/index.ts +8 -0
- package/src/server/rsc-entry/ssr-renderer.ts +11 -4
- package/src/server/ssr-bridge-types.ts +22 -2
- package/src/server/ssr-entry.ts +35 -28
- package/src/server/ssr-render.ts +13 -4
- package/src/server/ssr-wrappers.tsx +81 -61
- package/src/server/types.ts +10 -0
- package/src/shared/slot-params.ts +3 -4
- package/src/shims/navigation-rsc.ts +47 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
- package/dist/_chunks/cache-api-CQeYzA5g.js.map +0 -1
- package/dist/_chunks/convention-lint-DO10_pVl.js.map +0 -1
- package/dist/_chunks/href-validation-CMc5JRls.js.map +0 -1
- package/dist/_chunks/scanner-BRIOmHE2.js.map +0 -1
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +0 -1
- package/dist/_chunks/slot-params-BCTmZkQB.js +0 -76
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +0 -1
- package/dist/_chunks/ssr-data-Ya2HJPFp.js.map +0 -1
- package/dist/_chunks/use-segment-params-DzTBpkvj.js +0 -398
- package/dist/_chunks/use-segment-params-DzTBpkvj.js.map +0 -1
- package/docs/learn/04-loading-states.mdx +0 -67
- package/docs/learn/04b-the-flush-point.mdx +0 -115
- package/docs/learn/12-client-navigation.mdx +0 -176
- package/docs/learn/13-configuration.mdx +0 -166
- package/docs/more/01-advanced-routing.mdx +0 -344
- package/docs/more/02-advanced-forms.mdx +0 -137
- package/docs/more/03-coming-from-nextjs.mdx +0 -186
- package/docs/more/04-metadata-and-fonts.mdx +0 -193
- package/docs/more/04b-mdx.mdx +0 -229
- package/docs/more/05-content-collections.mdx +0 -90
- package/docs/more/06-instrumentation.mdx +0 -214
- package/docs/more/07-security.mdx +0 -129
- package/docs/more/08-developer-experience.mdx +0 -134
- package/docs/more/40-why-timber.mdx +0 -50
- package/docs/more/41-timber-vs-nextjs.mdx +0 -81
- package/docs/more/42-timber-vs-others.mdx +0 -68
- package/docs/more/50-ai-agent-instructions.mdx +0 -171
- /package/docs/learn/{06-forms-and-actions.mdx → 08-forms-and-actions.mdx} +0 -0
- /package/docs/learn/{09-caching.mdx → 10-caching.mdx} +0 -0
- /package/docs/learn/{11-error-handling.mdx → 12-error-handling.mdx} +0 -0
- /package/docs/learn/{14-deploying.mdx → 13-deploying.mdx} +0 -0
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Coming from Next.js'
|
|
3
|
-
description: 'A practical mapping for Next.js developers — in Next.js you did X, in timber you do Y.'
|
|
4
|
-
slug: 'coming-from-nextjs'
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Coming from Next.js
|
|
8
|
-
|
|
9
|
-
This page maps Next.js patterns to timber equivalents. It's not a comparison (see [timber vs Next.js](/docs/timber-vs-nextjs) for that) — it's a migration mental model.
|
|
10
|
-
|
|
11
|
-
## Data Fetching
|
|
12
|
-
|
|
13
|
-
**Next.js:** `getServerSideProps`, `getStaticProps`, or `fetch()` with implicit caching.
|
|
14
|
-
|
|
15
|
-
**timber:** Async server components. No loaders, no special functions. `fetch()` is never patched or cached — use `timber.cache()` explicitly when you want caching.
|
|
16
|
-
|
|
17
|
-
```tsx title="app/products/page.tsx"
|
|
18
|
-
// timber — just an async component
|
|
19
|
-
export default async function ProductsPage() {
|
|
20
|
-
const products = await db.products.findMany();
|
|
21
|
-
return <ProductList products={products} />;
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## `loading.tsx`
|
|
26
|
-
|
|
27
|
-
**Next.js:** `loading.tsx` convention inserts a Suspense boundary automatically.
|
|
28
|
-
|
|
29
|
-
**timber:** No `loading.tsx`. Place `<Suspense>` explicitly where you want it. This keeps you in control of what streams and what blocks the HTTP status code.
|
|
30
|
-
|
|
31
|
-
```tsx
|
|
32
|
-
// timber — explicit Suspense
|
|
33
|
-
import { Suspense } from 'react';
|
|
34
|
-
|
|
35
|
-
export default async function Page() {
|
|
36
|
-
const product = await getProduct(id); // blocks, can affect status code
|
|
37
|
-
return (
|
|
38
|
-
<div>
|
|
39
|
-
<h1>{product.name}</h1>
|
|
40
|
-
<Suspense fallback={<Skeleton />}>
|
|
41
|
-
<Reviews productId={id} /> {/* streams in */}
|
|
42
|
-
</Suspense>
|
|
43
|
-
</div>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Middleware
|
|
49
|
-
|
|
50
|
-
**Next.js:** Single global `middleware.ts` at the project root. Matcher patterns select routes.
|
|
51
|
-
|
|
52
|
-
**timber:** Per-segment `middleware.ts` files. Place them at the route level that matches the concern. `proxy.ts` for global concerns (CORS, rate limiting, security headers).
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
app/
|
|
56
|
-
proxy.ts # Global — every request (like Next.js middleware but with next())
|
|
57
|
-
dashboard/
|
|
58
|
-
middleware.ts # Only runs for /dashboard/*
|
|
59
|
-
settings/
|
|
60
|
-
middleware.ts # Only runs for /dashboard/settings/*
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## `useSearchParams`
|
|
64
|
-
|
|
65
|
-
**Next.js:** `useSearchParams()` hook returns a `URLSearchParams` object.
|
|
66
|
-
|
|
67
|
-
**timber:** Typed search params via `defineSearchParams` in any module you import. Server reads via `.get()`, client reads/writes via `.useQueryStates()`. Segment params are defined globally in `app/schema.ts` via `defineSchema()`, accessed with `getSegmentParams(SEGMENT_PATH)` using the `$segment` module.
|
|
68
|
-
|
|
69
|
-
```ts title="app/products/search-params.ts"
|
|
70
|
-
import { defineSearchParams } from '@timber-js/app/search-params';
|
|
71
|
-
import { z } from 'zod/v4';
|
|
72
|
-
|
|
73
|
-
export const searchParams = defineSearchParams({
|
|
74
|
-
page: z.coerce.number().default(1),
|
|
75
|
-
sort: z.enum(['price', 'name']).default('name'),
|
|
76
|
-
});
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Caching
|
|
80
|
-
|
|
81
|
-
**Next.js:** Implicit fetch caching, `unstable_cache`, ISR with `revalidate`.
|
|
82
|
-
|
|
83
|
-
**timber:** No implicit caching. No ISR. Explicit `cache.data()` with TTL and tags:
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
import { cache } from '@timber-js/app/cache';
|
|
87
|
-
|
|
88
|
-
declare const db: { products: { findPopular(): Promise<{ id: string; name: string }[]> } };
|
|
89
|
-
|
|
90
|
-
const getProducts = cache.data(
|
|
91
|
-
async () => db.products.findPopular(),
|
|
92
|
-
{ ttl: 300, tags: ['products'] }
|
|
93
|
-
);
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Authorization
|
|
97
|
-
|
|
98
|
-
**Next.js:** Usually done in middleware or layout components. No built-in access gate pattern.
|
|
99
|
-
|
|
100
|
-
**timber:** `access.ts` per segment. Runs inside the React tree with `React.cache` sharing. Supports slot degradation (a denied slot renders `denied.tsx` instead of failing the whole page).
|
|
101
|
-
|
|
102
|
-
```ts title="app/(auth)/access.ts"
|
|
103
|
-
import { redirect, getCookieJar } from '@timber-js/app/server';
|
|
104
|
-
|
|
105
|
-
declare function getSessionFromCookie(jar: ReturnType<typeof getCookieJar>): { userId: string } | null;
|
|
106
|
-
|
|
107
|
-
export default async function access() {
|
|
108
|
-
const session = getSessionFromCookie(getCookieJar());
|
|
109
|
-
if (!session) redirect('/login');
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Server Actions
|
|
114
|
-
|
|
115
|
-
**Next.js:** Server actions with `'use server'`. `useFormState` / `useFormStatus`.
|
|
116
|
-
|
|
117
|
-
**timber:** `createActionClient` for reusable middleware (auth, validation). `useActionState` from `@timber-js/app/client` returns a 4-tuple with auto-derived errors. Forms work without JavaScript via form flash.
|
|
118
|
-
|
|
119
|
-
```ts title="lib/action.ts"
|
|
120
|
-
import { createActionClient, ActionError } from '@timber-js/app/server';
|
|
121
|
-
import { getUser } from '@/lib/auth';
|
|
122
|
-
|
|
123
|
-
export const action = createActionClient({
|
|
124
|
-
middleware: async () => {
|
|
125
|
-
const user = await getUser();
|
|
126
|
-
if (!user) throw new ActionError('UNAUTHORIZED');
|
|
127
|
-
return { user };
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## `notFound()`
|
|
133
|
-
|
|
134
|
-
**Next.js:** `notFound()` function.
|
|
135
|
-
|
|
136
|
-
**timber:** `deny(404)` — sends a real HTTP 404 status code:
|
|
137
|
-
|
|
138
|
-
```tsx
|
|
139
|
-
import { deny } from '@timber-js/app/server';
|
|
140
|
-
|
|
141
|
-
if (!product) deny(404);
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
## `redirect()`
|
|
145
|
-
|
|
146
|
-
**Next.js:** `redirect()` from `next/navigation`.
|
|
147
|
-
|
|
148
|
-
**timber:** `redirect()` from `@timber-js/app/server`. Same concept, different import:
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
import { redirect } from '@timber-js/app/server';
|
|
152
|
-
|
|
153
|
-
redirect('/login'); // 302 by default
|
|
154
|
-
redirect('/login', 301); // Permanent redirect
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Code that imports `redirect()` or `notFound()` from `next/navigation` works unmodified — including calls during client component render, which timber converts into an SPA navigation (`redirect`) or your nearest 404 boundary (`notFound`), matching Next.js behavior.
|
|
158
|
-
|
|
159
|
-
## Metadata
|
|
160
|
-
|
|
161
|
-
**Next.js:** `export const metadata` or `export async function generateMetadata()`.
|
|
162
|
-
|
|
163
|
-
**timber:** Same pattern — export `metadata` (static) or a `metadata()` function (dynamic) from any page or layout.
|
|
164
|
-
|
|
165
|
-
## Imports
|
|
166
|
-
|
|
167
|
-
| Next.js import | timber import |
|
|
168
|
-
| ------------------------- | ------------------------------------------ |
|
|
169
|
-
| `next/link` | `@timber-js/app/client` (`Link`) |
|
|
170
|
-
| `next/navigation` | `@timber-js/app/client` (`useRouter`, etc) |
|
|
171
|
-
| `next/headers` | `@timber-js/app/server` (`getHeaders`) |
|
|
172
|
-
| `next/server` | `@timber-js/app/server` |
|
|
173
|
-
| `unstable_cache` | `@timber-js/app/cache` (`cache`) |
|
|
174
|
-
|
|
175
|
-
## Key Differences Summary
|
|
176
|
-
|
|
177
|
-
| Concept | Next.js | timber |
|
|
178
|
-
| -------------------- | -------------------------------- | ------------------------------------- |
|
|
179
|
-
| Fetch caching | Implicit, patched `fetch` | Explicit `timber.cache()` |
|
|
180
|
-
| ISR | Built-in | Not supported (use cache + TTL) |
|
|
181
|
-
| `loading.tsx` | Convention | Use explicit `<Suspense>` |
|
|
182
|
-
| Middleware | Single global file | Per-segment + global `proxy.ts` |
|
|
183
|
-
| Status codes | Often 200 for errors | Real HTTP status codes always |
|
|
184
|
-
| Auth | In middleware or components | `access.ts` with slot degradation |
|
|
185
|
-
| Search params | Untyped `URLSearchParams` | Typed codecs with validation |
|
|
186
|
-
| Forms without JS | Limited support | Full form flash support |
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Metadata and Fonts'
|
|
3
|
-
description: 'Page titles, Open Graph, metadata routes, Google Fonts, and local fonts.'
|
|
4
|
-
slug: 'metadata-and-fonts'
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Metadata and Fonts
|
|
8
|
-
|
|
9
|
-
## Metadata
|
|
10
|
-
|
|
11
|
-
Export `metadata` from any page or layout to control `<head>` tags.
|
|
12
|
-
|
|
13
|
-
### Static Metadata
|
|
14
|
-
|
|
15
|
-
```tsx title="app/page.tsx"
|
|
16
|
-
import type { Metadata } from '@timber-js/app/server';
|
|
17
|
-
|
|
18
|
-
export const metadata: Metadata = {
|
|
19
|
-
title: 'Dashboard',
|
|
20
|
-
description: 'Your project dashboard',
|
|
21
|
-
};
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### Dynamic Metadata
|
|
25
|
-
|
|
26
|
-
Export `metadata` as an async function to compute values at request time:
|
|
27
|
-
|
|
28
|
-
```tsx title="app/products/[id]/page.tsx"
|
|
29
|
-
import type { Metadata } from '@timber-js/app/server';
|
|
30
|
-
import { SEGMENT_PATH } from './$segment';
|
|
31
|
-
import { getSegmentParams } from '@timber-js/app/server';
|
|
32
|
-
|
|
33
|
-
declare function getProduct(id: string): Promise<{ name: string; summary: string; imageUrl: string }>;
|
|
34
|
-
|
|
35
|
-
export async function metadata(): Promise<Metadata> {
|
|
36
|
-
const { id } = getSegmentParams(SEGMENT_PATH);
|
|
37
|
-
const product = await getProduct(id);
|
|
38
|
-
return {
|
|
39
|
-
title: product.name,
|
|
40
|
-
description: product.summary,
|
|
41
|
-
openGraph: { images: [product.imageUrl] },
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Dynamic `metadata` runs during the render pass — `React.cache` is active, so data fetched here and in the page component is deduplicated.
|
|
47
|
-
|
|
48
|
-
### Title Templates
|
|
49
|
-
|
|
50
|
-
Layouts can define a title template. Pages fill in the `%s` placeholder:
|
|
51
|
-
|
|
52
|
-
```tsx title="app/layout.tsx"
|
|
53
|
-
export const metadata: Metadata = {
|
|
54
|
-
title: { template: '%s | My App', default: 'My App' },
|
|
55
|
-
};
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Open Graph & Twitter Cards
|
|
59
|
-
|
|
60
|
-
```tsx
|
|
61
|
-
export const metadata: Metadata = {
|
|
62
|
-
openGraph: {
|
|
63
|
-
title: 'My Product',
|
|
64
|
-
description: 'The best product ever.',
|
|
65
|
-
images: [{ url: '/og-image.png', width: 1200, height: 630 }],
|
|
66
|
-
},
|
|
67
|
-
twitter: {
|
|
68
|
-
card: 'summary_large_image',
|
|
69
|
-
title: 'My Product',
|
|
70
|
-
images: ['/og-image.png'],
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Metadata Routes
|
|
76
|
-
|
|
77
|
-
Special files in your route tree generate metadata endpoints:
|
|
78
|
-
|
|
79
|
-
| File | Output |
|
|
80
|
-
| --------------------- | ------------------------- |
|
|
81
|
-
| `sitemap.ts` | `/sitemap.xml` |
|
|
82
|
-
| `robots.ts` | `/robots.txt` |
|
|
83
|
-
| `manifest.ts` | `/manifest.json` |
|
|
84
|
-
| `opengraph-image.tsx` | `/opengraph-image.png` + auto `og:image` and `twitter:image` meta tags |
|
|
85
|
-
| `favicon.ico` / `favicon.tsx` | `/favicon.ico` |
|
|
86
|
-
| `icon.tsx` | `/icon.png` + auto `<link rel="icon">` |
|
|
87
|
-
|
|
88
|
-
When `opengraph-image.tsx` exists in a segment, timber automatically emits both `<meta property="og:image">` and `<meta name="twitter:image">` in `<head>` — no need to declare them in your `metadata` export. The auto-linked URL includes a cache-busting hash so social platforms pick up changes on redeploy.
|
|
89
|
-
|
|
90
|
-
If you declare `openGraph.images` in your metadata, both auto-linked tags are suppressed. If you declare only `twitter.images`, the auto-linked `twitter:image` is suppressed but `og:image` is still emitted. User-declared metadata always wins.
|
|
91
|
-
|
|
92
|
-
`favicon.tsx` lets you generate favicons dynamically (e.g. tenant branding). The URL stays `/favicon.ico` for browser compatibility.
|
|
93
|
-
|
|
94
|
-
For OG image generation, use `takumi-js/response` (BYO package — not bundled with timber):
|
|
95
|
-
|
|
96
|
-
```tsx title="app/opengraph-image.tsx"
|
|
97
|
-
import { ImageResponse } from 'takumi-js/response';
|
|
98
|
-
|
|
99
|
-
export default async function OpenGraphImage() {
|
|
100
|
-
return new ImageResponse(
|
|
101
|
-
<div style={{ fontSize: 48, background: 'white', width: '100%', height: '100%',
|
|
102
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
103
|
-
My App
|
|
104
|
-
</div>,
|
|
105
|
-
{ width: 1200, height: 630 }
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Metadata is always complete before the flush — no client-side injection, no partial `<head>`.
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## Fonts
|
|
115
|
-
|
|
116
|
-
timber.js downloads, subsets, and self-hosts fonts at build time. No requests to Google at runtime. No layout shift.
|
|
117
|
-
|
|
118
|
-
### Google Fonts
|
|
119
|
-
|
|
120
|
-
```tsx title="app/layout.tsx"
|
|
121
|
-
import { Inter, JetBrains_Mono } from '@timber-js/app/fonts/google';
|
|
122
|
-
|
|
123
|
-
const inter = Inter({
|
|
124
|
-
subsets: ['latin'],
|
|
125
|
-
display: 'swap',
|
|
126
|
-
variable: '--font-sans',
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
const jetbrainsMono = JetBrains_Mono({
|
|
130
|
-
subsets: ['latin'],
|
|
131
|
-
display: 'swap',
|
|
132
|
-
variable: '--font-mono',
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
136
|
-
return (
|
|
137
|
-
<html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
|
|
138
|
-
<body className={inter.className}>{children}</body>
|
|
139
|
-
</html>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Local Fonts
|
|
145
|
-
|
|
146
|
-
```tsx
|
|
147
|
-
import localFont from '@timber-js/app/fonts/local';
|
|
148
|
-
|
|
149
|
-
const myFont = localFont({
|
|
150
|
-
src: [
|
|
151
|
-
{ path: './fonts/MyFont-Regular.woff2', weight: '400' },
|
|
152
|
-
{ path: './fonts/MyFont-Bold.woff2', weight: '700' },
|
|
153
|
-
],
|
|
154
|
-
display: 'swap',
|
|
155
|
-
variable: '--font-custom',
|
|
156
|
-
});
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Static Analysis Constraints
|
|
160
|
-
|
|
161
|
-
Font calls look like runtime functions but are a build-time transform, so they must be statically analyzable. The canonical shape is a module-scope `const` (or `export const`) with a literal config object:
|
|
162
|
-
|
|
163
|
-
```tsx
|
|
164
|
-
const inter = Inter({ subsets: ['latin'] }); // ✅
|
|
165
|
-
export const mono = JetBrains_Mono({ subsets: ['latin'] }); // ✅
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Anything else fails the build with a descriptive error instead of silently breaking at runtime:
|
|
169
|
-
|
|
170
|
-
```tsx
|
|
171
|
-
export default Inter({ subsets: ['latin'] }); // ❌ build error
|
|
172
|
-
const theme = { font: Inter({ subsets: ['latin'] }) }; // ❌ build error
|
|
173
|
-
const cls = Inter({ subsets: ['latin'] }).className; // ❌ build error
|
|
174
|
-
import * as fonts from 'next/font/google'; // ❌ build error
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### CSS Variables
|
|
178
|
-
|
|
179
|
-
Use the `variable` option and reference it in your styles:
|
|
180
|
-
|
|
181
|
-
```css title="globals.css"
|
|
182
|
-
body { font-family: var(--font-sans), system-ui, sans-serif; }
|
|
183
|
-
code { font-family: var(--font-mono); }
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Next.js Compatibility
|
|
187
|
-
|
|
188
|
-
Imports from `next/font/google` and `next/font/local` are shimmed — existing code works without changes:
|
|
189
|
-
|
|
190
|
-
```tsx
|
|
191
|
-
import { Inter } from 'next/font/google';
|
|
192
|
-
import localFont from 'next/font/local';
|
|
193
|
-
```
|
package/docs/more/04b-mdx.mdx
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'MDX'
|
|
3
|
-
description: 'Use MDX to write pages in Markdown with embedded React components — server-rendered by default, zero client JS.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# MDX
|
|
7
|
-
|
|
8
|
-
MDX lets you write pages in Markdown with embedded React components. In timber.js, MDX pages are **server components by default** — they render on the server with zero client JavaScript unless a component explicitly opts in with `'use client'`.
|
|
9
|
-
|
|
10
|
-
## Setup
|
|
11
|
-
|
|
12
|
-
Add `'mdx'` to `pageExtensions` in your config:
|
|
13
|
-
|
|
14
|
-
```ts title="timber.config.ts"
|
|
15
|
-
export default {
|
|
16
|
-
pageExtensions: ['tsx', 'ts', 'jsx', 'js', 'mdx'],
|
|
17
|
-
};
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Install the MDX compiler — timber.js uses [Satteri](https://satteri.bruits.org), a Rust-based Markdown/MDX compiler with a native Vite plugin:
|
|
21
|
-
|
|
22
|
-
```bash title="Terminal"
|
|
23
|
-
pnpm add -D vite-plugin-satteri satteri
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
That's it. Any `page.mdx` file in your `app/` directory is now a route.
|
|
27
|
-
|
|
28
|
-
## Page-Level MDX
|
|
29
|
-
|
|
30
|
-
A `page.mdx` works exactly like `page.tsx` — it becomes a route based on its directory:
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
app/
|
|
34
|
-
docs/
|
|
35
|
-
getting-started/
|
|
36
|
-
page.mdx # /docs/getting-started
|
|
37
|
-
api-reference/
|
|
38
|
-
page.mdx # /docs/api-reference
|
|
39
|
-
layout.tsx # shared docs layout
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
MDX pages support `export const metadata` for page titles and descriptions:
|
|
43
|
-
|
|
44
|
-
````mdx title="app/docs/getting-started/page.mdx"
|
|
45
|
-
export const metadata = { title: 'Getting Started' }
|
|
46
|
-
|
|
47
|
-
# Getting Started
|
|
48
|
-
|
|
49
|
-
Install timber.js:
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
pnpm add @timber-js/app
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
This page is a **server component** — no JavaScript shipped to the browser.
|
|
56
|
-
````
|
|
57
|
-
|
|
58
|
-
## Frontmatter
|
|
59
|
-
|
|
60
|
-
Frontmatter parsing is built in — YAML between `---` fences or TOML between `+++` fences. Frontmatter is exported as a single `frontmatter` object:
|
|
61
|
-
|
|
62
|
-
````mdx title="app/blog/hello/page.mdx"
|
|
63
|
-
---
|
|
64
|
-
title: Hello World
|
|
65
|
-
description: My first post
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
# {frontmatter.title}
|
|
69
|
-
|
|
70
|
-
{frontmatter.description}
|
|
71
|
-
````
|
|
72
|
-
|
|
73
|
-
You can import the frontmatter object from another MDX file:
|
|
74
|
-
|
|
75
|
-
```tsx title="app/blog/layout.tsx"
|
|
76
|
-
import { frontmatter } from './page.mdx';
|
|
77
|
-
|
|
78
|
-
// frontmatter.title, frontmatter.description, etc.
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
For page metadata, use an explicit `export const metadata` — frontmatter alone does not set metadata automatically:
|
|
82
|
-
|
|
83
|
-
````mdx title="app/docs/getting-started/page.mdx"
|
|
84
|
-
---
|
|
85
|
-
title: Getting Started
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
export const metadata = { title: 'Getting Started' }
|
|
89
|
-
|
|
90
|
-
# {frontmatter.title}
|
|
91
|
-
````
|
|
92
|
-
|
|
93
|
-
## Custom Components
|
|
94
|
-
|
|
95
|
-
Create `mdx-components.tsx` at the project root to customize how Markdown elements render:
|
|
96
|
-
|
|
97
|
-
```tsx title="mdx-components.tsx"
|
|
98
|
-
import type { MDXComponents } from 'mdx/types';
|
|
99
|
-
|
|
100
|
-
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
|
101
|
-
return {
|
|
102
|
-
...components,
|
|
103
|
-
h1: (props) => <h1 className="text-3xl font-bold mt-8 mb-4" {...props} />,
|
|
104
|
-
pre: (props) => <pre className="bg-gray-900 text-gray-100 p-4 rounded-lg" {...props} />,
|
|
105
|
-
code: (props) => <code className="bg-gray-100 px-1.5 py-0.5 rounded text-sm" {...props} />,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
timber.js detects this file at startup and applies it to all MDX files automatically. The file can also live in `src/`.
|
|
111
|
-
|
|
112
|
-
## Using Client Components in MDX
|
|
113
|
-
|
|
114
|
-
MDX files stay on the server. To add interactivity, import a `'use client'` component:
|
|
115
|
-
|
|
116
|
-
````mdx title="app/docs/getting-started/page.mdx"
|
|
117
|
-
import { CopyButton } from '../../components/copy-button'
|
|
118
|
-
|
|
119
|
-
# Getting Started
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
pnpm add @timber-js/app
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
<CopyButton text="pnpm add @timber-js/app" />
|
|
126
|
-
````
|
|
127
|
-
|
|
128
|
-
Only the `CopyButton` ships JavaScript. The rest of the page renders as static HTML.
|
|
129
|
-
|
|
130
|
-
## Plugins and Features
|
|
131
|
-
|
|
132
|
-
GFM (tables, footnotes, strikethrough, task lists) and frontmatter are on by default. Additional syntax is enabled through `features`, and custom transforms are written as Satteri visitor plugins through the `mdx` key in your config:
|
|
133
|
-
|
|
134
|
-
```ts title="timber.config.ts"
|
|
135
|
-
import { defineHastPlugin } from 'satteri';
|
|
136
|
-
|
|
137
|
-
const externalLinks = defineHastPlugin({
|
|
138
|
-
name: 'external-links',
|
|
139
|
-
element: {
|
|
140
|
-
filter: ['a'],
|
|
141
|
-
visit(node, ctx) {
|
|
142
|
-
const href = node.properties.href;
|
|
143
|
-
if (typeof href === 'string' && href.startsWith('http')) {
|
|
144
|
-
ctx.setProperty(node, 'target', '_blank');
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
export default {
|
|
151
|
-
pageExtensions: ['tsx', 'ts', 'jsx', 'js', 'mdx'],
|
|
152
|
-
mdx: {
|
|
153
|
-
hastPlugins: [externalLinks],
|
|
154
|
-
features: { math: true },
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
The `mdx` config maps directly to `vite-plugin-satteri` options. Available fields:
|
|
160
|
-
|
|
161
|
-
| Option | Type | Description |
|
|
162
|
-
| -------------- | -------------------- | ------------------------------------------------------------------------------- |
|
|
163
|
-
| `mdastPlugins` | `MdastPluginInput[]` | Markdown AST visitors (created with `defineMdastPlugin`) |
|
|
164
|
-
| `hastPlugins` | `HastPluginInput[]` | HTML AST visitors (created with `defineHastPlugin`) |
|
|
165
|
-
| `features` | `Features` | Parser toggles — `gfm`, `frontmatter`, `math`, `directive`, `wikilinks`, … |
|
|
166
|
-
|
|
167
|
-
Satteri plugins are filtered visitors, not unified plugins — **remark/rehype plugins do not run** on Satteri's Rust-side AST. Visitors can be async and can replace nodes, which is enough to build things like shiki-based syntax highlighting (shiki transformers such as `@shikijs/twoslash` still work, since they run inside shiki).
|
|
168
|
-
|
|
169
|
-
If you need the unified MDX pipeline (for example, CodeHike), bypass timber's MDX support entirely: set `mdx: false` in your config and register `@mdx-js/rollup` yourself in `vite.config.ts` with `enforce: 'pre'`:
|
|
170
|
-
|
|
171
|
-
```ts title="timber.config.ts"
|
|
172
|
-
export default {
|
|
173
|
-
mdx: false, // disable timber's built-in MDX — bring your own compiler
|
|
174
|
-
};
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## Dynamic MDX Loading
|
|
178
|
-
|
|
179
|
-
For content-driven pages like a blog, use `import.meta.glob()` to build a map of MDX modules, then load the right one based on the URL:
|
|
180
|
-
|
|
181
|
-
```tsx title="app/blog/[slug]/page.tsx"
|
|
182
|
-
import { allBlogs } from 'content-collections';
|
|
183
|
-
import { deny, getSegmentParams } from '@timber-js/app/server';
|
|
184
|
-
import { SEGMENT_PATH } from './$segment';
|
|
185
|
-
|
|
186
|
-
const mdxModules = import.meta.glob<{ default: React.ComponentType }>(
|
|
187
|
-
'../../../content/blog/*.mdx'
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
export default async function BlogPost() {
|
|
191
|
-
const { slug } = getSegmentParams(SEGMENT_PATH);
|
|
192
|
-
const post = allBlogs.find((p) => p.slug === slug);
|
|
193
|
-
if (!post) deny(404);
|
|
194
|
-
|
|
195
|
-
const key = `../../../content/blog/${post._meta.fileName}`;
|
|
196
|
-
const loader = mdxModules[key];
|
|
197
|
-
if (!loader) deny(404);
|
|
198
|
-
|
|
199
|
-
const { default: MdxComponent } = await loader();
|
|
200
|
-
|
|
201
|
-
return (
|
|
202
|
-
<article>
|
|
203
|
-
<h1>{post.title}</h1>
|
|
204
|
-
<MdxComponent />
|
|
205
|
-
</article>
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
`import.meta.glob()` is resolved at build time by Vite — each `.mdx` file becomes a real ES module in the bundle with no `eval` or `new Function()` at runtime. The glob path is relative to the file that calls it.
|
|
211
|
-
|
|
212
|
-
This pattern pairs with [content collections](/docs/content-collections) — the collection provides typed metadata (title, date, tags), while `import.meta.glob()` loads the MDX body as a React component.
|
|
213
|
-
|
|
214
|
-
An alternative is `compileMDX()` from `@content-collections/mdx`, which compiles MDX to a serialized string during the content collection transform step. You then render it at runtime with `useMDXComponent(code)`. This avoids glob paths but trades them for a runtime eval of the compiled output. `import.meta.glob()` keeps everything as real ES modules resolved at build time.
|
|
215
|
-
|
|
216
|
-
## Content Collections
|
|
217
|
-
|
|
218
|
-
For structured content outside the route tree (blog posts, docs, changelogs), use [content collections](/docs/content-collections). Collections give you typed schemas, slug generation, and build-time validation — while MDX page routes are for pages that live directly in `app/`.
|
|
219
|
-
|
|
220
|
-
## Coming from Next.js
|
|
221
|
-
|
|
222
|
-
| Next.js | timber.js |
|
|
223
|
-
| ----------------------------------------- | --------------------------------------------- |
|
|
224
|
-
| `@next/mdx` wrapper package | Built-in — just add `'mdx'` to pageExtensions |
|
|
225
|
-
| `next.config.mjs` `withMDX()` wrapper | `timber.config.ts` `mdx` key |
|
|
226
|
-
| MDX pages are client components by default | MDX pages are server components by default |
|
|
227
|
-
| unified (remark/rehype) plugins | Satteri visitor plugins + built-in features |
|
|
228
|
-
| Custom loader for `.md` files | Built in — `.md` imports export an HTML string |
|
|
229
|
-
| `mdx-components.tsx` at project root | Same convention |
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: 'Content Collections'
|
|
3
|
-
description: 'Typed file-based content with Zod schemas, MDX rendering, and virtual module access.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Content Collections
|
|
7
|
-
|
|
8
|
-
Content collections let you define typed, file-based content in a `content/` directory. Each collection has a Zod schema for frontmatter validation and a transform for processing (like MDX compilation).
|
|
9
|
-
|
|
10
|
-
## Defining a Collection
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
// content-collections.ts
|
|
14
|
-
import { defineCollection, defineConfig } from '@content-collections/core';
|
|
15
|
-
import { compileMDX } from '@content-collections/mdx';
|
|
16
|
-
import { z } from 'zod/v4';
|
|
17
|
-
|
|
18
|
-
const docs = defineCollection({
|
|
19
|
-
name: 'docs',
|
|
20
|
-
directory: 'content/docs',
|
|
21
|
-
include: '**/*.{mdx,md}',
|
|
22
|
-
schema: z.object({
|
|
23
|
-
title: z.string(),
|
|
24
|
-
description: z.string(),
|
|
25
|
-
order: z.number(),
|
|
26
|
-
section: z.string().optional(),
|
|
27
|
-
}),
|
|
28
|
-
transform: async (document, context) => {
|
|
29
|
-
const mdx = await compileMDX(context, document);
|
|
30
|
-
return { ...document, mdx };
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export default defineConfig({ collections: [docs] });
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Using Collections
|
|
38
|
-
|
|
39
|
-
Import the generated collection and query it:
|
|
40
|
-
|
|
41
|
-
```tsx
|
|
42
|
-
import { allDocs } from 'content-collections';
|
|
43
|
-
import { getSegmentParams, deny } from '@timber-js/app/server';
|
|
44
|
-
|
|
45
|
-
declare const allDocs: { title: string; _meta: { fileName: string }; mdx: { default: React.ComponentType } }[];
|
|
46
|
-
|
|
47
|
-
export default async function DocsPage() {
|
|
48
|
-
const { slug } = getSegmentParams();
|
|
49
|
-
const doc = allDocs.find((d) => d._meta.fileName === slug);
|
|
50
|
-
if (!doc) deny(404);
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<article className="prose">
|
|
54
|
-
<h1>{doc.title}</h1>
|
|
55
|
-
<doc.mdx.default />
|
|
56
|
-
</article>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Content Directory Structure
|
|
62
|
-
|
|
63
|
-
```tsx
|
|
64
|
-
content / docs / v1 / getting - started.mdx;
|
|
65
|
-
routing.mdx;
|
|
66
|
-
blog / hello - world.mdx;
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Each file's `_meta` includes `path`, `fileName`, and `directory` — useful for filtering by version, building sidebar navigation, or generating static params.
|
|
70
|
-
|
|
71
|
-
## MDX Components
|
|
72
|
-
|
|
73
|
-
Customize MDX rendering with `mdx-components.tsx`:
|
|
74
|
-
|
|
75
|
-
```tsx
|
|
76
|
-
// mdx-components.tsx
|
|
77
|
-
import type { MDXComponents } from 'mdx/types';
|
|
78
|
-
|
|
79
|
-
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
|
80
|
-
return {
|
|
81
|
-
...components,
|
|
82
|
-
h1: ({ children }) => <h1 className="text-3xl font-bold">{children}</h1>,
|
|
83
|
-
code: ({ children }) => <code className="bg-gray-100 rounded px-1">{children}</code>,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Type Generation
|
|
89
|
-
|
|
90
|
-
Content collections generate TypeScript types at build time. Every document is fully typed — frontmatter fields, computed properties from transforms, and `_meta` information.
|