@timber-js/app 0.2.0-alpha.196 → 0.2.0-alpha.197
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-bE3H5Bjr.js} +3 -3
- package/dist/_chunks/{cli-check-BVthpfLS.js.map → cli-check-bE3H5Bjr.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-n3RJLgww.js} +2 -2
- package/dist/_chunks/{convention-lint-DO10_pVl.js.map → convention-lint-n3RJLgww.js.map} +1 -1
- 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/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/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/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,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.
|