diff --git a/.changeset/afraid-gifts-sparkle.md b/.changeset/afraid-gifts-sparkle.md
new file mode 100644
index 0000000000..ec33c1efcf
--- /dev/null
+++ b/.changeset/afraid-gifts-sparkle.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix UX issue about highlighting the search term in search result sections
diff --git a/.changeset/big-gorillas-perform.md b/.changeset/big-gorillas-perform.md
new file mode 100644
index 0000000000..88ec555a00
--- /dev/null
+++ b/.changeset/big-gorillas-perform.md
@@ -0,0 +1,9 @@
+---
+"gitbook": patch
+---
+
+Fix three small visual issues
+
+- Fix sidebar showing on `no-toc` pages in the gradient theme
+- Fix variant selector truncating incorrectly in header when sections are present
+- Fix page cover alignment on `lg` screens without TOC
diff --git a/.changeset/breezy-falcons-drop.md b/.changeset/breezy-falcons-drop.md
new file mode 100644
index 0000000000..834e46bfca
--- /dev/null
+++ b/.changeset/breezy-falcons-drop.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Respect fullWidth and defaultWidth for images
diff --git a/.changeset/clever-jokes-yell.md b/.changeset/clever-jokes-yell.md
new file mode 100644
index 0000000000..87b2698636
--- /dev/null
+++ b/.changeset/clever-jokes-yell.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list
diff --git a/.changeset/cold-buckets-divide.md b/.changeset/cold-buckets-divide.md
new file mode 100644
index 0000000000..1e528bad19
--- /dev/null
+++ b/.changeset/cold-buckets-divide.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+fix nested a tag causing hydration error
diff --git a/.changeset/cool-jars-matter.md b/.changeset/cool-jars-matter.md
new file mode 100644
index 0000000000..ff0c934f33
--- /dev/null
+++ b/.changeset/cool-jars-matter.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+fix href being empty in TOC
diff --git a/.changeset/cool-seas-approve.md b/.changeset/cool-seas-approve.md
new file mode 100644
index 0000000000..c97f692e6d
--- /dev/null
+++ b/.changeset/cool-seas-approve.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix navigation between sections/variants when previewing a site in v2
diff --git a/.changeset/curly-rules-learn.md b/.changeset/curly-rules-learn.md
new file mode 100644
index 0000000000..47400e9194
--- /dev/null
+++ b/.changeset/curly-rules-learn.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Add support for inline icons.
diff --git a/.changeset/fair-crews-wink.md b/.changeset/fair-crews-wink.md
new file mode 100644
index 0000000000..817854d1d7
--- /dev/null
+++ b/.changeset/fair-crews-wink.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Add circular corners and depth styling
diff --git a/.changeset/famous-melons-compete.md b/.changeset/famous-melons-compete.md
new file mode 100644
index 0000000000..17512b6f49
--- /dev/null
+++ b/.changeset/famous-melons-compete.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix crash when integration script fails to render block.
diff --git a/.changeset/fast-trees-battle.md b/.changeset/fast-trees-battle.md
new file mode 100644
index 0000000000..0e75fbbeda
--- /dev/null
+++ b/.changeset/fast-trees-battle.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Add authorization header for OAuth2
diff --git a/.changeset/fifty-ducks-press.md b/.changeset/fifty-ducks-press.md
new file mode 100644
index 0000000000..a250d31243
--- /dev/null
+++ b/.changeset/fifty-ducks-press.md
@@ -0,0 +1,6 @@
+---
+'@gitbook/react-openapi': patch
+'gitbook': patch
+---
+
+Improve support for OAuth2 security type
diff --git a/.changeset/flat-wolves-poke.md b/.changeset/flat-wolves-poke.md
new file mode 100644
index 0000000000..2b5c201cd1
--- /dev/null
+++ b/.changeset/flat-wolves-poke.md
@@ -0,0 +1,6 @@
+---
+"gitbook-v2": patch
+"gitbook": patch
+---
+
+Adds Columns layout block to GBO
diff --git a/.changeset/forty-readers-mix.md b/.changeset/forty-readers-mix.md
new file mode 100644
index 0000000000..98bede85d1
--- /dev/null
+++ b/.changeset/forty-readers-mix.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Support dark-mode specific page cover image
diff --git a/.changeset/fresh-shrimps-flow.md b/.changeset/fresh-shrimps-flow.md
new file mode 100644
index 0000000000..a2e62c9480
--- /dev/null
+++ b/.changeset/fresh-shrimps-flow.md
@@ -0,0 +1,5 @@
+---
+'gitbook': patch
+---
+
+Update Models page styling
diff --git a/.changeset/fuzzy-tables-jump.md b/.changeset/fuzzy-tables-jump.md
new file mode 100644
index 0000000000..ada33d1c55
--- /dev/null
+++ b/.changeset/fuzzy-tables-jump.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Optimize performances by using a smarter per-request cache arround data cached functions
diff --git a/.changeset/gorgeous-cycles-cheat.md b/.changeset/gorgeous-cycles-cheat.md
new file mode 100644
index 0000000000..d13c27765a
--- /dev/null
+++ b/.changeset/gorgeous-cycles-cheat.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+add a force-revalidate api route to force bust the cache in case of errors
diff --git a/.changeset/green-clouds-cough.md b/.changeset/green-clouds-cough.md
new file mode 100644
index 0000000000..0988603814
--- /dev/null
+++ b/.changeset/green-clouds-cough.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Fix rendering of ogimage with SVG logos.
diff --git a/.changeset/hip-bobcats-cover.md b/.changeset/hip-bobcats-cover.md
new file mode 100644
index 0000000000..9bd789f85d
--- /dev/null
+++ b/.changeset/hip-bobcats-cover.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Best effort at preserving current variant when navigating between sections by matching the pathname against site spaces in the new section.
diff --git a/.changeset/khaki-bees-count.md b/.changeset/khaki-bees-count.md
new file mode 100644
index 0000000000..a481690e24
--- /dev/null
+++ b/.changeset/khaki-bees-count.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix crash when integration is triggering invalid requests.
diff --git a/.changeset/lazy-pants-matter.md b/.changeset/lazy-pants-matter.md
new file mode 100644
index 0000000000..6852b473b9
--- /dev/null
+++ b/.changeset/lazy-pants-matter.md
@@ -0,0 +1,6 @@
+---
+"gitbook-v2": patch
+"gitbook": patch
+---
+
+encode customization header
diff --git a/.changeset/long-cameras-protect.md b/.changeset/long-cameras-protect.md
new file mode 100644
index 0000000000..358b9be33d
--- /dev/null
+++ b/.changeset/long-cameras-protect.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Rework full-width layout, add support for full-width page option
diff --git a/.changeset/nasty-moles-visit.md b/.changeset/nasty-moles-visit.md
new file mode 100644
index 0000000000..031fb81e30
--- /dev/null
+++ b/.changeset/nasty-moles-visit.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+fix ISR on preview env
diff --git a/.changeset/nervous-students-judge.md b/.changeset/nervous-students-judge.md
new file mode 100644
index 0000000000..a24d325b74
--- /dev/null
+++ b/.changeset/nervous-students-judge.md
@@ -0,0 +1,6 @@
+---
+"gitbook-v2": patch
+"gitbook": patch
+---
+
+Fix concurrent execution in Vercel causing pages to not be attached to the proper tags.
diff --git a/.changeset/orange-ears-drop.md b/.changeset/orange-ears-drop.md
new file mode 100644
index 0000000000..12dd7fae5d
--- /dev/null
+++ b/.changeset/orange-ears-drop.md
@@ -0,0 +1,5 @@
+---
+"@gitbook/react-contentkit": patch
+---
+
+Add basic error handling when transitioning between states.
diff --git a/.changeset/orange-hounds-sparkle.md b/.changeset/orange-hounds-sparkle.md
new file mode 100644
index 0000000000..28f2b6aa6e
--- /dev/null
+++ b/.changeset/orange-hounds-sparkle.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Generate a llms-full.txt version of the docs site
diff --git a/.changeset/pink-windows-wonder.md b/.changeset/pink-windows-wonder.md
new file mode 100644
index 0000000000..d8e9560dcc
--- /dev/null
+++ b/.changeset/pink-windows-wonder.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Don't crash ogimage generation on RTL text, as a workaround until we can support it.
diff --git a/.changeset/poor-dodos-lick.md b/.changeset/poor-dodos-lick.md
new file mode 100644
index 0000000000..ca02d7382b
--- /dev/null
+++ b/.changeset/poor-dodos-lick.md
@@ -0,0 +1,5 @@
+---
+"@gitbook/fonts": minor
+---
+
+Initial version of the package
diff --git a/.changeset/pretty-balloons-fold.md b/.changeset/pretty-balloons-fold.md
new file mode 100644
index 0000000000..b501879843
--- /dev/null
+++ b/.changeset/pretty-balloons-fold.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix rendering of ogimage when logo or icon are AVIF images.
diff --git a/.changeset/purple-cougars-breathe.md b/.changeset/purple-cougars-breathe.md
new file mode 100644
index 0000000000..2c6d010d4e
--- /dev/null
+++ b/.changeset/purple-cougars-breathe.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Add margin to adjacent buttons
diff --git a/.changeset/rare-pens-whisper.md b/.changeset/rare-pens-whisper.md
new file mode 100644
index 0000000000..aed0712ba4
--- /dev/null
+++ b/.changeset/rare-pens-whisper.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix ogimage generation failing with some JPEG images.
diff --git a/.changeset/real-walls-glow.md b/.changeset/real-walls-glow.md
new file mode 100644
index 0000000000..277c39ca9f
--- /dev/null
+++ b/.changeset/real-walls-glow.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Don't cache unexpected API errors for more than a few minutes.
diff --git a/.changeset/rich-buses-hunt.md b/.changeset/rich-buses-hunt.md
new file mode 100644
index 0000000000..fbc59d232d
--- /dev/null
+++ b/.changeset/rich-buses-hunt.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Fix an issue where PDF export URLs were not keeping their query params.
diff --git a/.changeset/rotten-donuts-bow.md b/.changeset/rotten-donuts-bow.md
new file mode 100644
index 0000000000..d6ebe0650a
--- /dev/null
+++ b/.changeset/rotten-donuts-bow.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+add a global error boundary
diff --git a/.changeset/rotten-seals-rush.md b/.changeset/rotten-seals-rush.md
new file mode 100644
index 0000000000..950e25880c
--- /dev/null
+++ b/.changeset/rotten-seals-rush.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Indent JSON python code sample
diff --git a/.changeset/sharp-hats-applaud.md b/.changeset/sharp-hats-applaud.md
new file mode 100644
index 0000000000..a3c5e8da9c
--- /dev/null
+++ b/.changeset/sharp-hats-applaud.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix missing title on button to close the announcement banner.
diff --git a/.changeset/sharp-jeans-burn.md b/.changeset/sharp-jeans-burn.md
new file mode 100644
index 0000000000..c990f8b7bf
--- /dev/null
+++ b/.changeset/sharp-jeans-burn.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Make icons for page groups more contrasting
diff --git a/.changeset/slimy-cows-press.md b/.changeset/slimy-cows-press.md
new file mode 100644
index 0000000000..17ec44ca8a
--- /dev/null
+++ b/.changeset/slimy-cows-press.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Ignore case while highlighting search results.
diff --git a/.changeset/slimy-hornets-share.md b/.changeset/slimy-hornets-share.md
new file mode 100644
index 0000000000..bfeef22793
--- /dev/null
+++ b/.changeset/slimy-hornets-share.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Consistently show variant selector in section bar if site has sections
diff --git a/.changeset/slow-lizards-obey.md b/.changeset/slow-lizards-obey.md
new file mode 100644
index 0000000000..ce1ef0e04c
--- /dev/null
+++ b/.changeset/slow-lizards-obey.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Make TOC height dynamic based on visible header and footer elements
diff --git a/.changeset/soft-walls-change.md b/.changeset/soft-walls-change.md
new file mode 100644
index 0000000000..2086de0317
--- /dev/null
+++ b/.changeset/soft-walls-change.md
@@ -0,0 +1,6 @@
+---
+"gitbook": patch
+"gitbook-v2": patch
+---
+
+Fix InlineLinkTooltip having a negative impact on performance, especially on larger pages.
diff --git a/.changeset/strong-poets-move.md b/.changeset/strong-poets-move.md
new file mode 100644
index 0000000000..9914aa44c9
--- /dev/null
+++ b/.changeset/strong-poets-move.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix bold header links hover color
diff --git a/.changeset/stupid-plums-perform.md b/.changeset/stupid-plums-perform.md
new file mode 100644
index 0000000000..34707627ea
--- /dev/null
+++ b/.changeset/stupid-plums-perform.md
@@ -0,0 +1,6 @@
+---
+"gitbook": patch
+"gitbook-v2": patch
+---
+
+cache fonts and static image used in OGImage in memory
diff --git a/.changeset/tame-mangos-battle.md b/.changeset/tame-mangos-battle.md
new file mode 100644
index 0000000000..844bf321a5
--- /dev/null
+++ b/.changeset/tame-mangos-battle.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix border being added to cards
diff --git a/.changeset/thick-chefs-repeat.md b/.changeset/thick-chefs-repeat.md
new file mode 100644
index 0000000000..8b08d50e3e
--- /dev/null
+++ b/.changeset/thick-chefs-repeat.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Handle nested deprecated properties in generateSchemaExample
diff --git a/.changeset/thick-cups-shout.md b/.changeset/thick-cups-shout.md
new file mode 100644
index 0000000000..3aabb9ad40
--- /dev/null
+++ b/.changeset/thick-cups-shout.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix crash during rendering of ogimage for VA sites with default icon.
diff --git a/.changeset/thin-buckets-grow.md b/.changeset/thin-buckets-grow.md
new file mode 100644
index 0000000000..84375c735d
--- /dev/null
+++ b/.changeset/thin-buckets-grow.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search
diff --git a/.changeset/tidy-dots-suffer.md b/.changeset/tidy-dots-suffer.md
new file mode 100644
index 0000000000..c622927590
--- /dev/null
+++ b/.changeset/tidy-dots-suffer.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+apply customization for dynamic context
diff --git a/.changeset/tiny-zoos-scream.md b/.changeset/tiny-zoos-scream.md
new file mode 100644
index 0000000000..1c80e7684e
--- /dev/null
+++ b/.changeset/tiny-zoos-scream.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Reverse order of feedback smileys
diff --git a/.changeset/violet-schools-care.md b/.changeset/violet-schools-care.md
new file mode 100644
index 0000000000..e77a98b5cf
--- /dev/null
+++ b/.changeset/violet-schools-care.md
@@ -0,0 +1,5 @@
+---
+'@gitbook/react-openapi': patch
+---
+
+Deduplicate path parameters from OpenAPI spec
diff --git a/.changeset/warm-roses-sleep.md b/.changeset/warm-roses-sleep.md
new file mode 100644
index 0000000000..e14b06cdc2
--- /dev/null
+++ b/.changeset/warm-roses-sleep.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Fix ogimage using incorrect Google Font depending on language.
diff --git a/.changeset/wise-gifts-smash.md b/.changeset/wise-gifts-smash.md
new file mode 100644
index 0000000000..32a85eecb7
--- /dev/null
+++ b/.changeset/wise-gifts-smash.md
@@ -0,0 +1,5 @@
+---
+"gitbook-v2": patch
+---
+
+remove trailing slash from linker
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index a65357a23d..b93b518961 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -63,9 +63,9 @@ To start your local version of GitBook, run the command `bun dev`.
 
 When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/`.
 
-For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/docs.gitbook.com` after running the development server.
+For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/gitbook.com/docs` after running the development server.
 
-You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://docs.gitbook.com/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
+You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
 
 ### Commit your update
 
diff --git a/.github/actions/gradual-deploy-cloudflare/action.yaml b/.github/actions/gradual-deploy-cloudflare/action.yaml
new file mode 100644
index 0000000000..eff064a754
--- /dev/null
+++ b/.github/actions/gradual-deploy-cloudflare/action.yaml
@@ -0,0 +1,83 @@
+name: Gradual Deploy to Cloudflare
+description: Use gradual deployment to deploy to Cloudflare. This action will upload the middleware and server versions to Cloudflare and kept them bound together
+inputs:
+  apiToken:
+    description: 'Cloudflare API token'
+    required: true
+  accountId:
+    description: 'Cloudflare account ID'
+    required: true
+  environment:
+    description: 'Cloudflare environment to deploy to (staging, production, preview)'
+    required: true
+  middlewareVersionId:
+    description: 'Middleware version ID to deploy'
+    required: true
+  serverVersionId:
+    description: 'Server version ID to deploy'
+    required: true
+outputs:
+  deployment-url: 
+    description: "Deployment URL"
+    value: ${{ steps.deploy_middleware.outputs.deployment-url }}
+runs:
+  using: 'composite'
+  steps:
+      - id: wrangler_status
+        name: Check wrangler deployment status
+        uses: cloudflare/wrangler-action@v3.14.0
+        with:
+            apiToken: ${{ inputs.apiToken }}
+            accountId: ${{ inputs.accountId }}
+            workingDirectory: ./
+            wranglerVersion: '4.10.0'
+            environment: ${{ inputs.environment }}
+            command: deployments status --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
+
+      # This step is used to get the version ID that is currently deployed to Cloudflare.
+      - id: extract_current_version
+        name: Extract current version
+        shell: bash
+        run: |
+          version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
+          echo "version_id=$version_id" >> $GITHUB_OUTPUT
+
+      - id: deploy_server
+        name: Deploy server to Cloudflare at 0%
+        uses: cloudflare/wrangler-action@v3.14.0
+        with:
+            apiToken: ${{ inputs.apiToken }}
+            accountId: ${{ inputs.accountId }}
+            workingDirectory: ./
+            wranglerVersion: '4.10.0'
+            environment: ${{ inputs.environment }}
+            command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
+
+      # Since we use version overrides headers, we can directly deploy the middleware to 100%.
+      - id: deploy_middleware
+        name: Deploy middleware to Cloudflare at 100%
+        uses: cloudflare/wrangler-action@v3.14.0
+        with:
+            apiToken: ${{ inputs.apiToken }}
+            accountId: ${{ inputs.accountId }}
+            workingDirectory: ./
+            wranglerVersion: '4.10.0'
+            environment: ${{ inputs.environment }}
+            command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
+
+      - name: Deploy server to Cloudflare at 100%
+        uses: cloudflare/wrangler-action@v3.14.0
+        with:
+            apiToken: ${{ inputs.apiToken }}
+            accountId: ${{ inputs.accountId }}
+            workingDirectory: ./
+            wranglerVersion: '4.10.0'
+            environment: ${{ inputs.environment }}
+            command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
+
+      - name: Outputs
+        shell: bash
+        env:
+          DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }}
+        run: |
+            echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}"
\ No newline at end of file
diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml
index fbc98fc82f..7e66a8bf33 100644
--- a/.github/composite/deploy-cloudflare/action.yaml
+++ b/.github/composite/deploy-cloudflare/action.yaml
@@ -28,7 +28,7 @@ inputs:
 outputs:
     deployment-url: 
       description: "Deployment URL"
-      value: ${{ steps.deploy.outputs.deployment-url }}
+      value: ${{ steps.upload_middleware.outputs.deployment-url }}
 runs:
     using: 'composite'
     steps:
@@ -63,8 +63,8 @@ runs:
           env:
             GITBOOK_RUNTIME: cloudflare
           shell: bash
-        - id: deploy
-          name: Deploy to Cloudflare
+
+        - name: Upload the DO worker
           uses: cloudflare/wrangler-action@v3.14.0
           with:
               apiToken: ${{ inputs.apiToken }}
@@ -72,10 +72,67 @@ runs:
               workingDirectory: ./
               wranglerVersion: '4.10.0'
               environment: ${{ inputs.environment }}
-              command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc
+              command: deploy --config ./packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
+
+        - id: upload_server
+          name: Upload server to Cloudflare
+          uses: cloudflare/wrangler-action@v3.14.0
+          with:
+              apiToken: ${{ inputs.apiToken }}
+              accountId: ${{ inputs.accountId }}
+              workingDirectory: ./
+              wranglerVersion: '4.10.0'
+              environment: ${{ inputs.environment }}
+              command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
+
+        - name: Extract server version worker ID
+          shell: bash
+          id: extract_server_version_id
+          run: |
+              version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
+              echo "version_id=$version_id" >> $GITHUB_OUTPUT
+
+        - name: Run updateWrangler scripts
+          shell: bash
+          run: |
+              bun run ./packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
+
+        - id: upload_middleware
+          name: Upload middleware to Cloudflare
+          uses: cloudflare/wrangler-action@v3.14.0
+          with:
+              apiToken: ${{ inputs.apiToken }}
+              accountId: ${{ inputs.accountId }}
+              workingDirectory: ./
+              wranglerVersion: '4.10.0'
+              environment: ${{ inputs.environment }}
+              command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
+        
+        - name: Extract middleware version worker ID
+          shell: bash
+          id: extract_middleware_version_id
+          run: |
+              version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
+              echo "version_id=$version_id" >> $GITHUB_OUTPUT
+
+        - name: Deploy server and middleware to Cloudflare
+          if: ${{ inputs.deploy == 'true' }}
+          uses: ./.github/actions/gradual-deploy-cloudflare
+          with:
+              apiToken: ${{ inputs.apiToken }}
+              accountId: ${{ inputs.accountId }}
+              opServiceAccount: ${{ inputs.opServiceAccount }}
+              opItem: ${{ inputs.opItem }}
+              environment: ${{ inputs.environment }}
+              serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
+              middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
+              deploy: ${{ inputs.deploy }}
+
+
         - name: Outputs
           shell: bash
           env:
-            DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
+            DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
           run: |
-              echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
\ No newline at end of file
+              echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
+              echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
\ No newline at end of file
diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml
index 156c1cfecc..c445004c0a 100644
--- a/.github/workflows/deploy-preview.yaml
+++ b/.github/workflows/deploy-preview.yaml
@@ -152,14 +152,14 @@ jobs:
 
                       | Site | `v1` | `2v` | `2c` |
                       | --- | --- | --- | --- |
-                      | GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com) |
+                      | GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs) |
                       | E2E | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) |
                   edit-mode: replace
     visual-testing-v1:
         runs-on: ubuntu-latest
         name: Visual Testing v1
         needs: deploy-v1-cloudflare
-        timeout-minutes: 8
+        timeout-minutes: 10
         steps:
             - name: Checkout
               uses: actions/checkout@v4
@@ -200,7 +200,6 @@ jobs:
         name: Visual Testing v2 (Cloudflare)
         needs: deploy-v2-cloudflare
         timeout-minutes: 10
-        if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main'
         steps:
             - name: Checkout
               uses: actions/checkout@v4
@@ -221,7 +220,7 @@ jobs:
         runs-on: ubuntu-latest
         name: Visual Testing Customers v1
         needs: deploy-v1-cloudflare
-        timeout-minutes: 8
+        timeout-minutes: 10
         steps:
             - name: Checkout
               uses: actions/checkout@v4
@@ -241,7 +240,7 @@ jobs:
         runs-on: ubuntu-latest
         name: Visual Testing Customers v2
         needs: deploy-v2-vercel
-        timeout-minutes: 8
+        timeout-minutes: 10
         steps:
             - name: Checkout
               uses: actions/checkout@v4
@@ -262,8 +261,7 @@ jobs:
         runs-on: ubuntu-latest
         name: Visual Testing Customers v2 (Cloudflare)
         needs: deploy-v2-cloudflare
-        timeout-minutes: 8
-        if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main'
+        timeout-minutes: 10
         steps:
             - name: Checkout
               uses: actions/checkout@v4
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b9d15df191..8a86e5d5d0 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -9,6 +9,7 @@
     ],
     "tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"],
     "prettier.enable": false,
+    "editor.formatOnSave": true,
     "editor.defaultFormatter": "biomejs.biome",
     "editor.codeActionsOnSave": {
         "source.organizeImports.biome": "explicit",
diff --git a/README.md b/README.md
index 5a055a7a75..9b13767ffd 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 <h1 align="center">GitBook</h1>
 
 <p align="center">
-  <a href="https://docs.gitbook.com/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a> 
+  <a href="https://gitbook.com/docs/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a> 
 </p>
 
 <p align="center">
@@ -56,17 +56,23 @@ git clone https://github.com/gitbookIO/gitbook.git
 bun install
 ```
 
-4. Start your local development server.
+4. Run build.
+
+```
+bun build:v2
+```
+
+5. Start your local development server.
 
 ```
 bun dev:v2
 ```
 
-5. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
+6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
 
 examples:
 
--   http://localhost:3000/url/docs.gitbook.com
+-   http://localhost:3000/url/gitbook.com/docs
 -   http://localhost:3000/url/open-source.gitbook.io/midjourney
 
 Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser.
@@ -150,11 +156,11 @@ See `LICENSE` for more information.
 </p>
 
 ```md
-[![GitBook](https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1)](https://gitbook.com/)
+[![GitBook](https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1)](https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link)
 ```
 
 ```html
-<a href="https://gitbook.com">
+<a href="https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link">
     <img
         src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"
     />
diff --git a/bun.lock b/bun.lock
index 323725884f..8afc0e702e 100644
--- a/bun.lock
+++ b/bun.lock
@@ -26,7 +26,7 @@
       "name": "@gitbook/cache-tags",
       "version": "0.3.1",
       "dependencies": {
-        "@gitbook/api": "^0.111.0",
+        "@gitbook/api": "catalog:",
         "assert-never": "^1.2.1",
       },
       "devDependencies": {
@@ -35,7 +35,7 @@
     },
     "packages/colors": {
       "name": "@gitbook/colors",
-      "version": "0.3.2",
+      "version": "0.3.3",
       "devDependencies": {
         "typescript": "^5.5.3",
       },
@@ -47,21 +47,34 @@
         "emoji-assets": "^8.0.0",
       },
     },
+    "packages/fonts": {
+      "name": "@gitbook/fonts",
+      "version": "0.0.0",
+      "dependencies": {
+        "@gitbook/api": "catalog:",
+      },
+      "devDependencies": {
+        "google-font-metadata": "^6.0.3",
+        "typescript": "^5.5.3",
+      },
+    },
     "packages/gitbook": {
       "name": "gitbook",
-      "version": "0.10.1",
+      "version": "0.12.0",
       "dependencies": {
-        "@gitbook/api": "*",
+        "@gitbook/api": "catalog:",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
         "@gitbook/emoji-codepoints": "workspace:*",
+        "@gitbook/fonts": "workspace:*",
         "@gitbook/icons": "workspace:*",
         "@gitbook/openapi-parser": "workspace:*",
         "@gitbook/react-contentkit": "workspace:*",
         "@gitbook/react-math": "workspace:*",
         "@gitbook/react-openapi": "workspace:*",
         "@radix-ui/react-checkbox": "^1.0.4",
+        "@radix-ui/react-dropdown-menu": "^2.1.12",
         "@radix-ui/react-navigation-menu": "^1.2.3",
         "@radix-ui/react-popover": "^1.0.7",
         "@radix-ui/react-tooltip": "^1.1.8",
@@ -72,25 +85,32 @@
         "assert-never": "^1.2.1",
         "bun-types": "^1.1.20",
         "classnames": "^2.5.1",
+        "direction": "^2.0.1",
         "event-iterator": "^2.0.0",
         "framer-motion": "^10.16.14",
+        "image-size": "^2.0.2",
         "js-cookie": "^3.0.5",
         "jsontoxml": "^1.0.1",
         "jwt-decode": "^4.0.0",
         "katex": "^0.16.9",
         "mathjax": "^3.2.2",
+        "mdast-util-from-markdown": "^2.0.2",
+        "mdast-util-frontmatter": "^2.0.1",
+        "mdast-util-gfm": "^3.1.0",
         "mdast-util-to-markdown": "^2.1.2",
         "memoizee": "^0.4.17",
+        "micromark-extension-frontmatter": "^2.0.0",
+        "micromark-extension-gfm": "^3.0.0",
         "next": "14.2.26",
         "next-themes": "^0.2.1",
         "nuqs": "^2.2.3",
         "object-hash": "^3.0.0",
         "openapi-types": "^12.1.3",
-        "p-map": "^7.0.0",
+        "p-map": "^7.0.3",
         "parse-cache-control": "^1.0.1",
         "partial-json": "^0.1.7",
-        "react": "18.3.1",
-        "react-dom": "18.3.1",
+        "react": "^19.0.0",
+        "react-dom": "^19.0.0",
         "react-hotkeys-hook": "^4.4.1",
         "rehype-sanitize": "^6.0.0",
         "rehype-stringify": "^10.0.1",
@@ -103,6 +123,8 @@
         "tailwind-merge": "^2.2.0",
         "tailwind-shades": "^1.1.2",
         "unified": "^11.0.5",
+        "unist-util-remove": "^4.0.0",
+        "unist-util-visit": "^5.0.0",
         "url-join": "^5.0.0",
         "usehooks-ts": "^3.1.0",
         "zod": "^3.24.2",
@@ -111,7 +133,7 @@
       },
       "devDependencies": {
         "@argos-ci/playwright": "^5.0.3",
-        "@cloudflare/next-on-pages": "1.13.7",
+        "@cloudflare/next-on-pages": "1.13.12",
         "@cloudflare/workers-types": "^4.20241230.0",
         "@playwright/test": "^1.51.1",
         "@types/js-cookie": "^3.0.6",
@@ -140,14 +162,16 @@
     },
     "packages/gitbook-v2": {
       "name": "gitbook-v2",
-      "version": "0.2.5",
+      "version": "0.3.0",
       "dependencies": {
-        "@gitbook/api": "*",
+        "@gitbook/api": "catalog:",
         "@gitbook/cache-tags": "workspace:*",
+        "@opennextjs/cloudflare": "1.2.1",
         "@sindresorhus/fnv1a": "^3.1.0",
         "assert-never": "^1.2.1",
         "jwt-decode": "^4.0.0",
-        "next": "canary",
+        "next": "^15.3.2",
+        "object-identity": "^0.1.2",
         "react": "^19.0.0",
         "react-dom": "^19.0.0",
         "rison": "^0.1.1",
@@ -155,7 +179,6 @@
         "warn-once": "^0.1.1",
       },
       "devDependencies": {
-        "@opennextjs/cloudflare": "^1.0.0-beta.3",
         "@types/rison": "^0.0.9",
         "gitbook": "*",
         "postcss": "^8",
@@ -184,7 +207,7 @@
     },
     "packages/openapi-parser": {
       "name": "@gitbook/openapi-parser",
-      "version": "2.1.3",
+      "version": "2.1.4",
       "dependencies": {
         "@scalar/openapi-parser": "^0.10.10",
         "@scalar/openapi-types": "^0.1.9",
@@ -201,7 +224,7 @@
       "name": "@gitbook/react-contentkit",
       "version": "0.7.0",
       "dependencies": {
-        "@gitbook/api": "*",
+        "@gitbook/api": "catalog:",
         "@gitbook/icons": "workspace:*",
         "classnames": "^2.5.1",
       },
@@ -231,7 +254,7 @@
     },
     "packages/react-openapi": {
       "name": "@gitbook/react-openapi",
-      "version": "1.1.10",
+      "version": "1.3.0",
       "dependencies": {
         "@gitbook/openapi-parser": "workspace:*",
         "@scalar/api-client-react": "^1.2.19",
@@ -259,9 +282,11 @@
   },
   "overrides": {
     "@codemirror/state": "6.4.1",
-    "@gitbook/api": "0.111.0",
-    "react": "18.3.1",
-    "react-dom": "18.3.1",
+    "react": "^19.0.0",
+    "react-dom": "^19.0.0",
+  },
+  "catalog": {
+    "@gitbook/api": "^0.121.0",
   },
   "packages": {
     "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
@@ -472,7 +497,7 @@
 
     "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
 
-    "@cloudflare/next-on-pages": ["@cloudflare/next-on-pages@1.13.7", "", { "dependencies": { "acorn": "^8.8.0", "ast-types": "^0.14.2", "chalk": "^5.2.0", "chokidar": "^3.5.3", "commander": "^11.1.0", "cookie": "^0.5.0", "esbuild": "^0.15.3", "js-yaml": "^4.1.0", "miniflare": "^3.20231218.1", "package-manager-manager": "^0.2.0", "pcre-to-regexp": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240208.0", "vercel": ">=30.0.0", "wrangler": "^3.28.2" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "next-on-pages": "bin/index.js" } }, "sha512-TSMVy+1fmxzeyykOC9guMEj7G2FgENw1T8V1sIFnah6piaJNBmybdyikPdQSdikP5w6v9eQhBt/TrXDPMDw0dw=="],
+    "@cloudflare/next-on-pages": ["@cloudflare/next-on-pages@1.13.12", "", { "dependencies": { "acorn": "^8.8.0", "ast-types": "^0.14.2", "chalk": "^5.2.0", "chokidar": "^3.5.3", "commander": "^11.1.0", "cookie": "^0.5.0", "esbuild": "^0.15.3", "js-yaml": "^4.1.0", "miniflare": "^3.20231218.1", "package-manager-manager": "^0.2.0", "pcre-to-regexp": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240208.0", "vercel": ">=30.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "next-on-pages": "bin/index.js" } }, "sha512-rPy7x9c2+0RDDdJ5o0TeRUwXJ1b7N1epnqF6qKSp5Wz1r9KHOyvaZh1ACoOC6Vu5k9su5WZOgy+8fPLIyrldMQ=="],
 
     "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.3.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.15", "workerd": "^1.20250320.0" }, "optionalPeers": ["workerd"] }, "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg=="],
 
@@ -546,10 +571,6 @@
 
     "@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="],
 
-    "@esbuild-plugins/node-globals-polyfill": ["@esbuild-plugins/node-globals-polyfill@0.2.3", "", { "peerDependencies": { "esbuild": "*" } }, "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw=="],
-
-    "@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="],
-
     "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="],
 
     "@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="],
@@ -600,6 +621,8 @@
 
     "@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="],
 
+    "@evan/concurrency": ["@evan/concurrency@0.0.3", "", {}, "sha512-vjhkm2nrXoM39G4aP/U4CC5vFv/ZlMRSjbTII0N65J9R0EpgjdGswnHKS1KSjfGAp/9zKSNaojBgi0SxKnGapw=="],
+
     "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
 
     "@floating-ui/core": ["@floating-ui/core@1.6.8", "", { "dependencies": { "@floating-ui/utils": "^0.2.8" } }, "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA=="],
@@ -628,7 +651,7 @@
 
     "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
 
-    "@gitbook/api": ["@gitbook/api@0.111.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-E5Pk28kPD4p6XNWdwFM9pgDijdByseIZQqcFK+/hoW5tEZa5Yw/plRKJyN1hmwfPL6SKq6Maf0fbIzTQiVXyQQ=="],
+    "@gitbook/api": ["@gitbook/api@0.121.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-o4/N24RM0Rg8S/3yPDjPmt6TbQF+1iZmg9q9QKxOxMqpQ2bZmMUqS7dSkeqEbEBMALx/m/x0xQlJbEJGbOwteg=="],
 
     "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
 
@@ -640,6 +663,8 @@
 
     "@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.8", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^6.6.0" } }, "sha512-i4PgiuGyUb52Muhc52kK3aMJIMfMkA2RbPW30tre8a6M8T6mWTfYo6gafSgjNvF1vH29zcuB8oBYnF0gO4XcHA=="],
 
+    "@gitbook/fonts": ["@gitbook/fonts@workspace:packages/fonts"],
+
     "@gitbook/icons": ["@gitbook/icons@workspace:packages/icons"],
 
     "@gitbook/openapi-parser": ["@gitbook/openapi-parser@workspace:packages/openapi-parser"],
@@ -794,9 +819,25 @@
 
     "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 
-    "@opennextjs/aws": ["@opennextjs/aws@3.5.7", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-YjyHJrkIHI7YwQRCp8GjDOudu86oOc1RiwxvBBpPHrplsS18H4ZmkzGggAKhK6B4myGsJQ/q9kNP2TraoZiNzg=="],
+    "@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw=="],
+
+    "@octokit/core": ["@octokit/core@6.1.5", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg=="],
+
+    "@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
+
+    "@octokit/graphql": ["@octokit/graphql@8.2.2", "", { "dependencies": { "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA=="],
+
+    "@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
+
+    "@octokit/request": ["@octokit/request@9.2.3", "", { "dependencies": { "@octokit/endpoint": "^10.1.4", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w=="],
 
-    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.0-beta.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.5.7", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^3.114.3 || ^4.7.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-qKBXQZhUeQ+iGvfJeF7PO30g59LHnPOlRVZd77zxwn6Uc9C+c0LSwo8N28XRIWyQPkY007rKk9pSIxOrP4MHtQ=="],
+    "@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
+
+    "@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
+
+    "@opennextjs/aws": ["@opennextjs/aws@3.6.5", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-wni+CWlRCyWfhNfekQBBPPkrDDnaGdZLN9hMybKI0wKOKTO+zhPOqR65Eh3V0pzWAi84Sureb5mdMuLwCxAAcw=="],
+
+    "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.2.1", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.5", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.19.1" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-cOco+nHwlo/PLB1bThF8IIvaS8PgAT9MEI5ZttFO/qt6spgvr2lUaPkpjgSIQmI3sBIEG2cLUykvQ2nbbZEcVw=="],
 
     "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
@@ -822,11 +863,15 @@
 
     "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="],
 
+    "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.12", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VJoMs+BWWE7YhzEQyVwvF9n22Eiyr83HotCVrMQzla/OwRovXCgah7AcaEr4hMNj4gJxSdtIbcHGvmJXOoJVHA=="],
+
     "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
 
     "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA=="],
 
-    "@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
+    "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
+
+    "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.7", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.4", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.4", "@radix-ui/react-portal": "1.1.6", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-roving-focus": "1.1.7", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+qYq6LfbiGo97Zz9fioX83HCiIYYFNs8zAsVCMQrIakoNYylIzWuoD/anAD3UzvvR6cnswmfRFJFq/zYYq/k7Q=="],
 
     "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wUi01RrTDTOoGtjEPHsxlzPtVzVc3R/AZ5wfh0dyqMAqolhHAHvG5iQjBCTi2AjQqa77FWWbA3kE3RkD+bDMgQ=="],
 
@@ -840,6 +885,8 @@
 
     "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="],
 
+    "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q=="],
+
     "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="],
 
     "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
@@ -848,6 +895,8 @@
 
     "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="],
 
+    "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
+
     "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
 
     "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="],
@@ -1094,7 +1143,7 @@
 
     "@scalar/object-utils": ["@scalar/object-utils@1.1.13", "", { "dependencies": { "flatted": "^3.3.1", "just-clone": "^6.2.0", "ts-deepmerge": "^7.0.1" } }, "sha512-311eTykIXgOtjCs4VTELj9UMT97jHTWc5qkGNoIzZ5nxjCcvOVe7kDQobIkE8dGT+ybOgHz5qly02Eu7nVHeZQ=="],
 
-    "@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.10", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-6MSgvpNKu/anZy96dn8tXQZo1PuDCoeB4m2ZLLDS4vC2zaTnuNBvvQHx+gjwXNKWhTbIVy8bQpYBzlMAYnFNcQ=="],
+    "@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.14", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-VXr979NMx6wZ+kpFKor2eyCJZOjyMwcBRc6c4Gc92ZMOC7ZNYqjwbw+Ubh2ELJyP5cWAjOFSrNwtylema0pw5w=="],
 
     "@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="],
 
@@ -1390,7 +1439,7 @@
 
     "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
 
-    "acorn": ["acorn@8.12.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="],
+    "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
 
     "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
 
@@ -1458,6 +1507,8 @@
 
     "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
 
+    "before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="],
+
     "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="],
 
     "bignumber.js": ["bignumber.js@9.1.2", "", {}, "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="],
@@ -1470,6 +1521,8 @@
 
     "body-parser": ["body-parser@2.0.2", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "3.1.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.5.2", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "^3.0.0", "type-is": "~1.6.18" } }, "sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ=="],
 
+    "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
+
     "bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="],
 
     "boxen": ["boxen@4.2.0", "", { "dependencies": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", "chalk": "^3.0.0", "cli-boxes": "^2.2.0", "string-width": "^4.1.0", "term-size": "^2.1.0", "type-fest": "^0.8.1", "widest-line": "^3.1.0" } }, "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ=="],
@@ -1494,6 +1547,8 @@
 
     "bytes": ["bytes@3.1.0", "", {}, "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="],
 
+    "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
+
     "cacheable": ["cacheable@1.8.9", "", { "dependencies": { "hookified": "^1.7.1", "keyv": "^5.3.1" } }, "sha512-FicwAUyWnrtnd4QqYAoRlNs44/a1jTL7XDKqm5gJ90wz1DQPlC7U2Rd1Tydpv+E7WAr4sQHuw8Q8M3nZMAyecQ=="],
 
     "cacheable-request": ["cacheable-request@6.1.0", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^3.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^4.1.0", "responselike": "^1.0.2" } }, "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg=="],
@@ -1510,8 +1565,6 @@
 
     "caniuse-lite": ["caniuse-lite@1.0.30001668", "", {}, "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw=="],
 
-    "capnp-ts": ["capnp-ts@0.7.0", "", { "dependencies": { "debug": "^4.3.1", "tslib": "^2.2.0" } }, "sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g=="],
-
     "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
 
     "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
@@ -1564,8 +1617,6 @@
 
     "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
 
-    "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
-
     "configstore": ["configstore@5.0.1", "", { "dependencies": { "dot-prop": "^5.2.0", "graceful-fs": "^4.1.2", "make-dir": "^3.0.0", "unique-string": "^2.0.0", "write-file-atomic": "^3.0.0", "xdg-basedir": "^4.0.0" } }, "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA=="],
 
     "consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
@@ -1594,10 +1645,16 @@
 
     "css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="],
 
+    "css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="],
+
     "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
 
+    "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="],
+
     "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
 
+    "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="],
+
     "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
 
     "cva": ["cva@1.0.0-beta.2", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "typescript": ">= 4.5.5 < 6" }, "optionalPeers": ["typescript"] }, "sha512-dqcOFe247I5pKxfuzqfq3seLL5iMYsTgo40Uw7+pKZAntPgFtR7Tmy59P5IVIq/XgB0NQWoIvYDt9TwHkuK8Cg=="],
@@ -1652,8 +1709,18 @@
 
     "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
 
+    "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
+
     "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
 
+    "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
+
+    "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
+
+    "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
+
+    "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
+
     "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
 
     "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="],
@@ -1686,7 +1753,7 @@
 
     "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
 
-    "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+    "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
 
     "env-cmd": ["env-cmd@10.1.0", "", { "dependencies": { "commander": "^4.0.0", "cross-spawn": "^7.0.0" }, "bin": { "env-cmd": "bin/env-cmd.js" } }, "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA=="],
 
@@ -1756,7 +1823,7 @@
 
     "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
 
-    "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+    "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
 
     "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="],
 
@@ -1792,6 +1859,8 @@
 
     "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
 
+    "fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="],
+
     "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
 
     "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -1808,6 +1877,8 @@
 
     "fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="],
 
+    "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="],
+
     "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
 
     "fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
@@ -1834,6 +1905,8 @@
 
     "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
 
+    "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
+
     "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
 
     "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
@@ -1892,6 +1965,8 @@
 
     "google-auth-library": ["google-auth-library@5.10.1", "", { "dependencies": { "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^2.1.0", "gcp-metadata": "^3.4.0", "gtoken": "^4.1.0", "jws": "^4.0.0", "lru-cache": "^5.0.0" } }, "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg=="],
 
+    "google-font-metadata": ["google-font-metadata@6.0.3", "", { "dependencies": { "@evan/concurrency": "^0.0.3", "@octokit/core": "^6.1.2", "cac": "^6.7.14", "consola": "^3.3.3", "deepmerge": "^4.3.1", "json-stringify-pretty-compact": "^4.0.0", "linkedom": "^0.18.6", "pathe": "^1.1.2", "picocolors": "^1.1.1", "playwright": "^1.49.1", "stylis": "^4.3.4", "zod": "^3.24.1" }, "bin": { "gfm": "dist/cli.mjs" } }, "sha512-62+QB+nmBKppHrxwvILZkV/EvKctEAdzkKBIJY26TtwZkUjeEqmAQnE5uHvtMTdB8odqGpQu5LAKPqqUYHI9wA=="],
+
     "google-p12-pem": ["google-p12-pem@2.0.5", "", { "dependencies": { "node-forge": "^0.10.0" }, "bin": { "gp12-pem": "build/src/bin/gp12-pem.js" } }, "sha512-7RLkxwSsMsYh9wQ5Vb2zRtkAHvqPvfoMGag+nugl1noYO7gf0844Yr9TIFA5NEBMAeVt2Z+Imu7CQMp3oNatzQ=="],
 
     "googleapis": ["googleapis@47.0.0", "", { "dependencies": { "google-auth-library": "^5.6.1", "googleapis-common": "^3.2.0" } }, "sha512-+Fnjgcc3Na/rk57dwxqW1V0HJXJFjnt3aqFlckULqAqsPkmex/AyJJe6MSlXHC37ZmlXEb9ZtPGUp5ZzRDXpHg=="],
@@ -1968,12 +2043,16 @@
 
     "hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="],
 
+    "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
+
     "html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="],
 
     "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
 
     "html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="],
 
+    "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="],
+
     "http-cache-semantics": ["http-cache-semantics@4.1.1", "", {}, "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="],
 
     "http-errors": ["http-errors@1.4.0", "", { "dependencies": { "inherits": "2.0.1", "statuses": ">= 1.2.1 < 2" } }, "sha512-oLjPqve1tuOl5aRhv8GK5eHpqP1C9fb+Ol+XTLjKfLltE44zdDbEdjPSbU7Ch5rSNsVFqZn97SrMmZLdu1/YMw=="],
@@ -1992,6 +2071,8 @@
 
     "ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="],
 
+    "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
+
     "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
 
     "import-lazy": ["import-lazy@2.1.0", "", {}, "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A=="],
@@ -2084,6 +2165,8 @@
 
     "json-stringify-deterministic": ["json-stringify-deterministic@1.0.12", "", {}, "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g=="],
 
+    "json-stringify-pretty-compact": ["json-stringify-pretty-compact@4.0.0", "", {}, "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="],
+
     "json-xml-parse": ["json-xml-parse@1.3.0", "", {}, "sha512-MVosauc/3W2wL4dd4yaJzH5oXw+HOUfptn0+d4+bFghMiJFop7MaqIwFXJNLiRnNYJNQ6L4o7B+53n5wcvoLFw=="],
 
     "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
@@ -2122,6 +2205,8 @@
 
     "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
 
+    "linkedom": ["linkedom@0.18.11", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" } }, "sha512-K03GU3FUlnhBAP0jPb7tN7YJl7LbjZx30Z8h6wgLXusnKF7+BEZvfEbdkN/lO9LfFzxN3S0ZAriDuJ/13dIsLA=="],
+
     "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
 
     "lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="],
@@ -2180,9 +2265,11 @@
 
     "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA=="],
 
-    "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
+    "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
+
+    "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="],
 
-    "mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="],
+    "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
 
     "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
 
@@ -2226,6 +2313,8 @@
 
     "micromark-core-commonmark": ["micromark-core-commonmark@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA=="],
 
+    "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="],
+
     "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
 
     "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
@@ -2308,8 +2397,6 @@
 
     "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
 
-    "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="],
-
     "mnemonist": ["mnemonist@0.38.3", "", { "dependencies": { "obliterator": "^1.6.1" } }, "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw=="],
 
     "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
@@ -2350,12 +2437,16 @@
 
     "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
 
+    "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
+
     "nuqs": ["nuqs@2.2.3", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "@remix-run/react": ">=2", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router-dom": ">=6" }, "optionalPeers": ["@remix-run/react", "next", "react-router-dom"] }, "sha512-nMCcUW06KSqEXA0xp+LiRqDpIE59BVYbjZLe0HUisJAlswfihHYSsAjYTzV0lcE1thfh8uh+LqUHGdQ8qq8rfA=="],
 
     "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
 
     "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
 
+    "object-identity": ["object-identity@0.1.2", "", {}, "sha512-Px5puVllX5L2aBjbcfXpiG5xXeq6OE8RckryTeP2Zq+0PgYrCGJXmC6LblWgknKSJs11Je2W4U2NOWFj3t/QXQ=="],
+
     "object-inspect": ["object-inspect@1.13.2", "", {}, "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="],
 
     "object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
@@ -2396,7 +2487,7 @@
 
     "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
 
-    "p-map": ["p-map@7.0.2", "", {}, "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q=="],
+    "p-map": ["p-map@7.0.3", "", {}, "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA=="],
 
     "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
 
@@ -2440,7 +2531,7 @@
 
     "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
 
-    "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+    "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
 
     "pcre-to-regexp": ["pcre-to-regexp@1.1.0", "", {}, "sha512-KF9XxmUQJ2DIlMj3TqNqY1AWvyvTuIuq11CuuekxyaYMiFuMKGgQrePYMX5bXKLhLG3sDI4CsGAYHPaT7VV7+g=="],
 
@@ -2454,8 +2545,6 @@
 
     "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="],
 
-    "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
-
     "playwright": ["playwright@1.51.1", "", { "dependencies": { "playwright-core": "1.51.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw=="],
 
     "playwright-core": ["playwright-core@1.51.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw=="],
@@ -2518,13 +2607,13 @@
 
     "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
 
-    "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
+    "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
 
     "react-aria": ["react-aria@3.37.0", "", { "dependencies": { "@internationalized/string": "^3.2.5", "@react-aria/breadcrumbs": "^3.5.20", "@react-aria/button": "^3.11.1", "@react-aria/calendar": "^3.7.0", "@react-aria/checkbox": "^3.15.1", "@react-aria/color": "^3.0.3", "@react-aria/combobox": "^3.11.1", "@react-aria/datepicker": "^3.13.0", "@react-aria/dialog": "^3.5.21", "@react-aria/disclosure": "^3.0.1", "@react-aria/dnd": "^3.8.1", "@react-aria/focus": "^3.19.1", "@react-aria/gridlist": "^3.10.1", "@react-aria/i18n": "^3.12.5", "@react-aria/interactions": "^3.23.0", "@react-aria/label": "^3.7.14", "@react-aria/link": "^3.7.8", "@react-aria/listbox": "^3.14.0", "@react-aria/menu": "^3.17.0", "@react-aria/meter": "^3.4.19", "@react-aria/numberfield": "^3.11.10", "@react-aria/overlays": "^3.25.0", "@react-aria/progress": "^3.4.19", "@react-aria/radio": "^3.10.11", "@react-aria/searchfield": "^3.8.0", "@react-aria/select": "^3.15.1", "@react-aria/selection": "^3.22.0", "@react-aria/separator": "^3.4.5", "@react-aria/slider": "^3.7.15", "@react-aria/ssr": "^3.9.7", "@react-aria/switch": "^3.6.11", "@react-aria/table": "^3.16.1", "@react-aria/tabs": "^3.9.9", "@react-aria/tag": "^3.4.9", "@react-aria/textfield": "^3.16.0", "@react-aria/tooltip": "^3.7.11", "@react-aria/utils": "^3.27.0", "@react-aria/visually-hidden": "^3.8.19", "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-u3WUEMTcbQFaoHauHO3KhPaBYzEv1o42EdPcLAs05GBw9Q6Axlqwo73UFgMrsc2ElwLAZ4EKpSdWHLo1R5gfiw=="],
 
     "react-aria-components": ["react-aria-components@1.6.0", "", { "dependencies": { "@internationalized/date": "^3.7.0", "@internationalized/string": "^3.2.5", "@react-aria/autocomplete": "3.0.0-alpha.37", "@react-aria/collections": "3.0.0-alpha.7", "@react-aria/color": "^3.0.3", "@react-aria/disclosure": "^3.0.1", "@react-aria/dnd": "^3.8.1", "@react-aria/focus": "^3.19.1", "@react-aria/interactions": "^3.23.0", "@react-aria/live-announcer": "^3.4.1", "@react-aria/menu": "^3.17.0", "@react-aria/toolbar": "3.0.0-beta.12", "@react-aria/tree": "3.0.0-beta.3", "@react-aria/utils": "^3.27.0", "@react-aria/virtualizer": "^4.1.1", "@react-stately/autocomplete": "3.0.0-alpha.0", "@react-stately/color": "^3.8.2", "@react-stately/disclosure": "^3.0.1", "@react-stately/layout": "^4.1.1", "@react-stately/menu": "^3.9.1", "@react-stately/selection": "^3.19.0", "@react-stately/table": "^3.13.1", "@react-stately/utils": "^3.10.5", "@react-stately/virtualizer": "^4.2.1", "@react-types/color": "^3.0.2", "@react-types/form": "^3.7.9", "@react-types/grid": "^3.2.11", "@react-types/shared": "^3.27.0", "@react-types/table": "^3.10.4", "@swc/helpers": "^0.5.0", "client-only": "^0.0.1", "react-aria": "^3.37.0", "react-stately": "^3.35.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-YfG9PUE7XrXtDDAqT4pLTGyYQaiHHTBFdAK/wNgGsypVnQSdzmyYlV3Ty8aHlZJI6hP9RWkbywvosXkU7KcPHg=="],
 
-    "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
+    "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
 
     "react-hotkeys-hook": ["react-hotkeys-hook@4.5.1", "", { "peerDependencies": { "react": ">=16.8.1", "react-dom": ">=16.8.1" } }, "sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg=="],
 
@@ -2598,12 +2687,6 @@
 
     "rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="],
 
-    "rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="],
-
-    "rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="],
-
-    "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="],
-
     "router": ["router@2.0.0", "", { "dependencies": { "array-flatten": "3.0.0", "is-promise": "4.0.0", "methods": "~1.1.2", "parseurl": "~1.3.3", "path-to-regexp": "^8.0.0", "setprototypeof": "1.2.0", "utils-merge": "1.0.1" } }, "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ=="],
 
     "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@@ -2612,7 +2695,7 @@
 
     "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
 
-    "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
+    "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
 
     "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
 
@@ -2660,8 +2743,6 @@
 
     "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
 
-    "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="],
-
     "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
 
     "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="],
@@ -2718,6 +2799,8 @@
 
     "stylelint": ["stylelint@16.16.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "@csstools/media-query-list-parser": "^4.0.2", "@csstools/selector-specificity": "^5.0.0", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.3.7", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^10.0.7", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", "ignore": "^7.0.3", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.35.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.3", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", "supports-hyperlinks": "^3.2.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^5.0.1" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-40X5UOb/0CEFnZVEHyN260HlSSUxPES+arrUphOumGWgXERHfwCD0kNBVILgQSij8iliYVwlc0V7M5bcLP9vPg=="],
 
+    "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
+
     "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
 
     "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -2822,6 +2905,8 @@
 
     "ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
 
+    "uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="],
+
     "uid-promise": ["uid-promise@1.0.0", "", {}, "sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig=="],
 
     "undici": ["undici@5.28.4", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g=="],
@@ -2840,12 +2925,16 @@
 
     "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
 
+    "unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="],
+
     "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
 
     "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
 
     "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
 
+    "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
+
     "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
 
     "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@@ -3558,15 +3647,13 @@
 
     "@changesets/parse/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
 
-    "@cloudflare/next-on-pages/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="],
-
     "@cloudflare/next-on-pages/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
 
     "@cloudflare/next-on-pages/esbuild": ["esbuild@0.15.18", "", { "optionalDependencies": { "@esbuild/android-arm": "0.15.18", "@esbuild/linux-loong64": "0.15.18", "esbuild-android-64": "0.15.18", "esbuild-android-arm64": "0.15.18", "esbuild-darwin-64": "0.15.18", "esbuild-darwin-arm64": "0.15.18", "esbuild-freebsd-64": "0.15.18", "esbuild-freebsd-arm64": "0.15.18", "esbuild-linux-32": "0.15.18", "esbuild-linux-64": "0.15.18", "esbuild-linux-arm": "0.15.18", "esbuild-linux-arm64": "0.15.18", "esbuild-linux-mips64le": "0.15.18", "esbuild-linux-ppc64le": "0.15.18", "esbuild-linux-riscv64": "0.15.18", "esbuild-linux-s390x": "0.15.18", "esbuild-netbsd-64": "0.15.18", "esbuild-openbsd-64": "0.15.18", "esbuild-sunos-64": "0.15.18", "esbuild-windows-32": "0.15.18", "esbuild-windows-64": "0.15.18", "esbuild-windows-arm64": "0.15.18" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q=="],
 
-    "@cloudflare/next-on-pages/miniflare": ["miniflare@3.20241018.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "^8.8.0", "acorn-walk": "^8.2.0", "capnp-ts": "^0.7.0", "exit-hook": "^2.2.1", "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", "workerd": "1.20241018.1", "ws": "^8.17.1", "youch": "^3.2.2", "zod": "^3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-g7i5oGAoJOk8+hJp77A5/wAdu7PEvi5hQc+0wzwzjhUNM2I5DHd2Cc29ACPhAe1kIXvCCVkxs3+REF52qnX0aw=="],
+    "@cloudflare/next-on-pages/miniflare": ["miniflare@3.20250214.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250214.0", "ws": "8.18.0", "youch": "3.2.3", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-t+lT4p2lbOcKv4PS3sx1F/wcDAlbEYZCO2VooLp4H7JErWWYIi9yjD3UillC3CGOpiBahVg5nrPCoFltZf6UlA=="],
 
-    "@cloudflare/next-on-pages/wrangler": ["wrangler@3.112.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250214.2", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.1", "workerd": "1.20250214.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250214.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-PNQWGze3ODlWwG33LPr8kNhbht3eB3L9fogv+fapk2fjaqj0kNweRapkwmvtz46ojcqWzsxmTe4nOC0hIVUfPA=="],
+    "@cloudflare/next-on-pages/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
 
     "@codemirror/lang-html/@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA=="],
 
@@ -3662,7 +3749,9 @@
 
     "@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
 
-    "@opennextjs/aws/esbuild": ["esbuild@0.19.2", "", { "optionalDependencies": { "@esbuild/android-arm": "0.19.2", "@esbuild/android-arm64": "0.19.2", "@esbuild/android-x64": "0.19.2", "@esbuild/darwin-arm64": "0.19.2", "@esbuild/darwin-x64": "0.19.2", "@esbuild/freebsd-arm64": "0.19.2", "@esbuild/freebsd-x64": "0.19.2", "@esbuild/linux-arm": "0.19.2", "@esbuild/linux-arm64": "0.19.2", "@esbuild/linux-ia32": "0.19.2", "@esbuild/linux-loong64": "0.19.2", "@esbuild/linux-mips64el": "0.19.2", "@esbuild/linux-ppc64": "0.19.2", "@esbuild/linux-riscv64": "0.19.2", "@esbuild/linux-s390x": "0.19.2", "@esbuild/linux-x64": "0.19.2", "@esbuild/netbsd-x64": "0.19.2", "@esbuild/openbsd-x64": "0.19.2", "@esbuild/sunos-x64": "0.19.2", "@esbuild/win32-arm64": "0.19.2", "@esbuild/win32-ia32": "0.19.2", "@esbuild/win32-x64": "0.19.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg=="],
+    "@opennextjs/aws/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
+
+    "@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
 
     "@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
 
@@ -3676,24 +3765,88 @@
 
     "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="],
 
+    "@radix-ui/react-dropdown-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
+
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="],
+
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
+
+    "@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+    "@radix-ui/react-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.4", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
+
+    "@radix-ui/react-menu/react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
+
     "@radix-ui/react-navigation-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
 
     "@radix-ui/react-navigation-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
 
+    "@radix-ui/react-navigation-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
+
     "@radix-ui/react-navigation-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="],
 
     "@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="],
 
     "@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ=="],
 
+    "@radix-ui/react-popover/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
+
     "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A=="],
 
+    "@radix-ui/react-roving-focus/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
+
     "@radix-ui/react-tooltip/@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
 
     "@radix-ui/react-tooltip/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
 
     "@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
 
+    "@radix-ui/react-tooltip/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
+
     "@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="],
 
     "@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
@@ -3706,6 +3859,8 @@
 
     "@radix-ui/react-tooltip/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q=="],
 
+    "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
     "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="],
 
     "@react-aria/focus/clsx": ["clsx@2.0.0", "", {}, "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="],
@@ -3714,14 +3869,10 @@
 
     "@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
 
-    "@scalar/api-client/@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.14", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-VXr979NMx6wZ+kpFKor2eyCJZOjyMwcBRc6c4Gc92ZMOC7ZNYqjwbw+Ubh2ELJyP5cWAjOFSrNwtylema0pw5w=="],
-
     "@scalar/api-client/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
 
     "@scalar/api-client/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="],
 
-    "@scalar/import/@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.14", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-VXr979NMx6wZ+kpFKor2eyCJZOjyMwcBRc6c4Gc92ZMOC7ZNYqjwbw+Ubh2ELJyP5cWAjOFSrNwtylema0pw5w=="],
-
     "@scalar/oas-utils/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
 
     "@scalar/object-utils/flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="],
@@ -3922,8 +4073,6 @@
 
     "@vercel/gatsby-plugin-vercel-builder/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="],
 
-    "@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
-
     "@vercel/nft/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
 
     "@vercel/nft/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
@@ -3948,6 +4097,8 @@
 
     "@vercel/static-config/ajv": ["ajv@8.6.3", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw=="],
 
+    "@vue/compiler-core/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
     "@vue/compiler-sfc/postcss": ["postcss@8.4.47", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", "source-map-js": "^1.2.1" } }, "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ=="],
 
     "@vueuse/integrations/@vueuse/core": ["@vueuse/core@11.2.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "11.2.0", "@vueuse/shared": "11.2.0", "vue-demi": ">=0.14.10" } }, "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA=="],
@@ -3980,8 +4131,6 @@
 
     "cacheable-request/lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
 
-    "capnp-ts/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
-
     "codemirror/@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA=="],
 
     "codemirror/@codemirror/commands": ["@codemirror/commands@6.7.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-+cduIZ2KbesDhbykV02K25A5xIVrquSPz4UxxYBemRlAT2aW8dhwUgLDwej7q/RJUHKk4nALYcR1puecDvbdqw=="],
@@ -3998,6 +4147,8 @@
 
     "decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="],
 
+    "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
     "edge-runtime/async-listen": ["async-listen@3.0.1", "", {}, "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA=="],
 
     "edge-runtime/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="],
@@ -4032,7 +4183,7 @@
 
     "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
 
-    "gitbook-v2/next": ["next@15.3.1-canary.8", "", { "dependencies": { "@next/env": "15.3.1-canary.8", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.1-canary.8", "@next/swc-darwin-x64": "15.3.1-canary.8", "@next/swc-linux-arm64-gnu": "15.3.1-canary.8", "@next/swc-linux-arm64-musl": "15.3.1-canary.8", "@next/swc-linux-x64-gnu": "15.3.1-canary.8", "@next/swc-linux-x64-musl": "15.3.1-canary.8", "@next/swc-win32-arm64-msvc": "15.3.1-canary.8", "@next/swc-win32-x64-msvc": "15.3.1-canary.8", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Of5a3BTTIl/iUvL2a9Jh7m7G/H8z4Pj5Vs54CLvcdadokxSNgLOpjzbDgFR8J4PawLx6+MOMy19m9Cvr6EPGug=="],
+    "gitbook-v2/next": ["next@15.3.2", "", { "dependencies": { "@next/env": "15.3.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.2", "@next/swc-darwin-x64": "15.3.2", "@next/swc-linux-arm64-gnu": "15.3.2", "@next/swc-linux-arm64-musl": "15.3.2", "@next/swc-linux-x64-gnu": "15.3.2", "@next/swc-linux-x64-musl": "15.3.2", "@next/swc-win32-arm64-msvc": "15.3.2", "@next/swc-win32-x64-msvc": "15.3.2", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ=="],
 
     "global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="],
 
@@ -4046,6 +4197,8 @@
 
     "google-auth-library/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
 
+    "google-font-metadata/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
     "googleapis-common/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="],
 
     "got/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="],
@@ -4070,16 +4223,20 @@
 
     "make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 
-    "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
-
-    "mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
+    "mdast-util-gfm-footnote/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
 
     "mdast-util-gfm-footnote/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
 
+    "mdast-util-gfm-strikethrough/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
+
     "mdast-util-gfm-strikethrough/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
 
+    "mdast-util-gfm-table/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
+
     "mdast-util-gfm-table/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
 
+    "mdast-util-gfm-task-list-item/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
+
     "mdast-util-gfm-task-list-item/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
 
     "meow/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
@@ -4090,8 +4247,6 @@
 
     "micromark/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
 
-    "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
-
     "miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="],
 
     "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
@@ -4102,8 +4257,6 @@
 
     "minizlib/minipass": ["minipass@2.9.0", "", { "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg=="],
 
-    "mlly/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
-
     "next/@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="],
 
     "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
@@ -4114,6 +4267,8 @@
 
     "package-json/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 
+    "parse5/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
     "path-match/path-to-regexp": ["path-to-regexp@1.9.0", "", { "dependencies": { "isarray": "0.0.1" } }, "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g=="],
 
     "path-scurry/lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="],
@@ -4152,15 +4307,13 @@
 
     "read-yaml-file/pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
 
-    "remark-stringify/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
+    "remark-gfm/mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="],
 
-    "rimraf/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
+    "remark-parse/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
 
-    "rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
-
-    "rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
+    "remark-stringify/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
 
-    "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
+    "rimraf/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
 
     "router/is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
 
@@ -4200,14 +4353,16 @@
 
     "terminal-link/supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="],
 
-    "terser/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
-
     "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
 
+    "ts-node/acorn": ["acorn@8.12.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="],
+
     "ts-node/acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
 
     "ts-node/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
 
+    "unenv/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
     "update-notifier/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
 
     "url-parse-lax/prepend-http": ["prepend-http@2.0.0", "", {}, "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="],
@@ -4650,21 +4805,13 @@
 
     "@cloudflare/next-on-pages/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.15.18", "", { "os": "linux", "cpu": "none" }, "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ=="],
 
-    "@cloudflare/next-on-pages/miniflare/acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
+    "@cloudflare/next-on-pages/miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="],
 
-    "@cloudflare/next-on-pages/miniflare/workerd": ["workerd@1.20241018.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20241018.1", "@cloudflare/workerd-darwin-arm64": "1.20241018.1", "@cloudflare/workerd-linux-64": "1.20241018.1", "@cloudflare/workerd-linux-arm64": "1.20241018.1", "@cloudflare/workerd-windows-64": "1.20241018.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-JPW2oAbYOnJj1c5boyDOdjl/Yvur45jhVE8lf+I9oxR6myyAvuH2tdXO62kye68jRluJOMUeyssLes+JRwLmaA=="],
+    "@cloudflare/next-on-pages/miniflare/workerd": ["workerd@1.20250214.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250214.0", "@cloudflare/workerd-darwin-arm64": "1.20250214.0", "@cloudflare/workerd-linux-64": "1.20250214.0", "@cloudflare/workerd-linux-arm64": "1.20250214.0", "@cloudflare/workerd-windows-64": "1.20250214.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g=="],
 
-    "@cloudflare/next-on-pages/miniflare/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
+    "@cloudflare/next-on-pages/miniflare/youch": ["youch@3.2.3", "", { "dependencies": { "cookie": "^0.5.0", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw=="],
 
-    "@cloudflare/next-on-pages/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="],
-
-    "@cloudflare/next-on-pages/wrangler/miniflare": ["miniflare@3.20250214.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250214.0", "ws": "8.18.0", "youch": "3.2.3", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-t+lT4p2lbOcKv4PS3sx1F/wcDAlbEYZCO2VooLp4H7JErWWYIi9yjD3UillC3CGOpiBahVg5nrPCoFltZf6UlA=="],
-
-    "@cloudflare/next-on-pages/wrangler/unenv": ["unenv@2.0.0-rc.1", "", { "dependencies": { "defu": "^6.1.4", "mlly": "^1.7.4", "ohash": "^1.1.4", "pathe": "^1.1.2", "ufo": "^1.5.4" } }, "sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg=="],
-
-    "@cloudflare/next-on-pages/wrangler/workerd": ["workerd@1.20250214.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250214.0", "@cloudflare/workerd-darwin-arm64": "1.20250214.0", "@cloudflare/workerd-linux-64": "1.20250214.0", "@cloudflare/workerd-linux-arm64": "1.20250214.0", "@cloudflare/workerd-windows-64": "1.20250214.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g=="],
+    "@cloudflare/next-on-pages/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
 
     "@codemirror/lang-json/@codemirror/language/@codemirror/view": ["@codemirror/view@6.34.1", "", { "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ=="],
 
@@ -4696,54 +4843,96 @@
 
     "@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.2", "", { "os": "android", "cpu": "arm" }, "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q=="],
+    "@opennextjs/aws/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
+
+    "@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.2", "", { "os": "android", "cpu": "arm64" }, "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw=="],
+    "@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.2", "", { "os": "android", "cpu": "x64" }, "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w=="],
+    "@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA=="],
+    "@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw=="],
+    "@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ=="],
+    "@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw=="],
+    "@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.2", "", { "os": "linux", "cpu": "arm" }, "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.2", "", { "os": "linux", "cpu": "x64" }, "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ=="],
+    "@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.2", "", { "os": "none", "cpu": "x64" }, "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ=="],
+    "@opennextjs/aws/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw=="],
+    "@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw=="],
+    "@opennextjs/aws/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg=="],
+    "@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA=="],
+    "@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
 
-    "@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.2", "", { "os": "win32", "cpu": "x64" }, "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw=="],
+    "@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
+
+    "@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
+
+    "@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
 
     "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
 
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
+
+    "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+    "@radix-ui/react-menu/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+    "@radix-ui/react-menu/react-remove-scroll/react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
+
+    "@radix-ui/react-menu/react-remove-scroll/react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
+
+    "@radix-ui/react-menu/react-remove-scroll/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+    "@radix-ui/react-menu/react-remove-scroll/use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
+
+    "@radix-ui/react-menu/react-remove-scroll/use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
+
     "@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
 
+    "@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
+
+    "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
     "@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
 
     "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
@@ -4908,23 +5097,23 @@
 
     "gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
 
-    "gitbook-v2/next/@next/env": ["@next/env@15.3.1-canary.8", "", {}, "sha512-ShZTo0hNhbTRrp7k6oUDSck4Xx4hhfSeLBp35jvGaw1QMZzWYr5v/oc0kEt0bfMdl+833flwKV7kFR3BnrULfg=="],
+    "gitbook-v2/next/@next/env": ["@next/env@15.3.2", "", {}, "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g=="],
 
-    "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.1-canary.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZaDynM+pbrnLLlBAxH/CDGp9KN79OFrLcT1ejlWyo86V3SS9Gyqr4nmTuvTevByTTpr1VHReQel8Zbq0Pttu7Q=="],
+    "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g=="],
 
-    "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.1-canary.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-BsMR8WqeCDAX8C9RYAO8TI4ttpuqKk2oYMb1+bCrOYi857SMfB4vWD4PWmMvj7mwFXGqrxly4W6CBQlD66A+fg=="],
+    "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w=="],
 
-    "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.1-canary.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-xL+K2SW+/46j/KnKNf1gizM1bxwcEaE56eCEG9RPoYS/lfxHLuHcR9O2MlcPI90g/rIN2HXmHeuKRbXbnVy15g=="],
+    "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA=="],
 
-    "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.1-canary.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-bzXlCUXkjIRsMTb6rr7OsWEQmdO2rZgKijnMGBJzEpa9ROq95VJTF+rdyrLmHuc4fPsAGDn/C18V3E1YOi4ipQ=="],
+    "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg=="],
 
-    "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.1-canary.8", "", { "os": "linux", "cpu": "x64" }, "sha512-snbPQ9th7eoYYMZpNGhEvX4EbqGjjkiXzTEm2F/oDadlZR2wI6egtfQHiZqeczL7XwG3M66hBJ1/U20wdTDtvA=="],
+    "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg=="],
 
-    "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.1-canary.8", "", { "os": "linux", "cpu": "x64" }, "sha512-5hmcaGazc3w6rg/gbQOrmuw0kKShf4Egs0JlUPop/ZiRDfZO5KlyrGY0hUD+2pLG7Yx3B/fTbj86cHKfQZDMBw=="],
+    "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w=="],
 
-    "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.1-canary.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-t6uwWC/UbQ8CQGyBVbEmpcJC41yAvz2eZZGec9EoO0ZyQ/fbvjOkqfPnf+WcjWifWNeuiWogjN1UBg7YK9IaVA=="],
+    "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ=="],
 
-    "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.1-canary.8", "", { "os": "win32", "cpu": "x64" }, "sha512-93gNqVYwlr9bz6Pm5dQnqjpuOEQyvDre0+H39UqS7h2KuFh3sf2MejhAEr2uFEux4mb7TN0PlohaihO7YxJ3fw=="],
+    "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA=="],
 
     "gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
 
@@ -4952,6 +5141,10 @@
 
     "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
 
+    "remark-gfm/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="],
+
+    "remark-gfm/mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
+
     "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
 
     "rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -5114,81 +5307,15 @@
 
     "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
 
-    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20241018.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-CRySEzjNRoR8frP5AbtJXd1tgVJa5v7bZon9Dh6nljYlhG+piDv8jvOVEUqF3cXXS+M5aXwr4NlozdMvl5g5mg=="],
-
-    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20241018.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y63yWJNTgETDFkY3Ony71/k/G1HRDFIhEzwbT+OWmg1Qbsqa4TquHPVFkgv+OJhpmD3HV9gTBcn/M2QJ/+pGmg=="],
-
-    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20241018.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a2AbSAXNMMiREvN+PwjHdJ5zOzI4+qf8+rb6H/y4HcVbPZN5C2fanxv5Bx7NUHLiMD/W0FrGug1aU+RPUVZC9Q=="],
-
-    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20241018.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-iCJ7bjD/+zhlp3IWnkiry180DwdNvak/sVoS98pIAS41aR3gJVzE5BCz/2yTWFdCoUVZ5yKJrv1HhSKgQRBIEw=="],
-
-    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20241018.1", "", { "os": "win32", "cpu": "x64" }, "sha512-qwDVh/KrwEPY82h6tZ1O4BXBAKeGy30BeTr9wvTUVeY9eX/KT73GuEG+ttwiashRfqjOa0Gcqjsfpd913ITFyg=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.17.19", "", { "os": "android", "cpu": "x64" }, "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.17.19", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.17.19", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.17.19", "", { "os": "linux", "cpu": "arm" }, "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.17.19", "", { "os": "linux", "cpu": "ia32" }, "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.17.19", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.17.19", "", { "os": "linux", "cpu": "s390x" }, "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.17.19", "", { "os": "none", "cpu": "x64" }, "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.17.19", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.17.19", "", { "os": "sunos", "cpu": "x64" }, "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.17.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.17.19", "", { "os": "win32", "cpu": "ia32" }, "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw=="],
-
-    "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="],
-
-    "@cloudflare/next-on-pages/wrangler/miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
-
-    "@cloudflare/next-on-pages/wrangler/miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="],
-
-    "@cloudflare/next-on-pages/wrangler/miniflare/youch": ["youch@3.2.3", "", { "dependencies": { "cookie": "^0.5.0", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw=="],
-
-    "@cloudflare/next-on-pages/wrangler/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
-
-    "@cloudflare/next-on-pages/wrangler/unenv/ohash": ["ohash@1.1.4", "", {}, "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="],
-
-    "@cloudflare/next-on-pages/wrangler/unenv/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
-
-    "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250214.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q=="],
+    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250214.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q=="],
 
-    "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250214.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw=="],
+    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250214.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw=="],
 
-    "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250214.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ=="],
+    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250214.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ=="],
 
-    "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250214.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA=="],
+    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250214.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA=="],
 
-    "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250214.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ=="],
+    "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250214.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ=="],
 
     "@node-minify/core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
diff --git a/package.json b/package.json
index 736eabe411..1cf5326c1d 100644
--- a/package.json
+++ b/package.json
@@ -7,12 +7,11 @@
         "turbo": "^2.5.0",
         "vercel": "^39.3.0"
     },
-    "packageManager": "bun@1.2.8",
+    "packageManager": "bun@1.2.15",
     "overrides": {
         "@codemirror/state": "6.4.1",
-        "react": "18.3.1",
-        "react-dom": "18.3.1",
-        "@gitbook/api": "0.111.0"
+        "react": "^19.0.0",
+        "react-dom": "^19.0.0"
     },
     "private": true,
     "scripts": {
@@ -34,7 +33,12 @@
         "download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
         "clean": "turbo run clean"
     },
-    "workspaces": ["packages/*"],
+    "workspaces": {
+        "packages": ["packages/*"],
+        "catalog": {
+            "@gitbook/api": "^0.121.0"
+        }
+    },
     "patchedDependencies": {
         "decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
         "@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json
index 3d25aa8f48..a57048f2f6 100644
--- a/packages/cache-tags/package.json
+++ b/packages/cache-tags/package.json
@@ -10,7 +10,7 @@
     },
     "version": "0.3.1",
     "dependencies": {
-        "@gitbook/api": "^0.111.0",
+        "@gitbook/api": "catalog:",
         "assert-never": "^1.2.1"
     },
     "devDependencies": {
diff --git a/packages/colors/CHANGELOG.md b/packages/colors/CHANGELOG.md
index 0c1606b101..1695a6c97d 100644
--- a/packages/colors/CHANGELOG.md
+++ b/packages/colors/CHANGELOG.md
@@ -1,5 +1,13 @@
 # @gitbook/colors
 
+## 0.3.3
+
+### Patch Changes
+
+- c3f6b8c: Update chroma ratio per step
+- 5e975ab: Fix code highlighting for HTTP
+- f7a3470: Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast
+
 ## 0.3.2
 
 ### Patch Changes
diff --git a/packages/colors/package.json b/packages/colors/package.json
index 36cd0f47f8..16f54d1ff3 100644
--- a/packages/colors/package.json
+++ b/packages/colors/package.json
@@ -8,7 +8,7 @@
             "default": "./dist/index.js"
         }
     },
-    "version": "0.3.2",
+    "version": "0.3.3",
     "devDependencies": {
         "typescript": "^5.5.3"
     },
diff --git a/packages/colors/src/transformations.ts b/packages/colors/src/transformations.ts
index bf7f770c09..8234a460a9 100644
--- a/packages/colors/src/transformations.ts
+++ b/packages/colors/src/transformations.ts
@@ -214,13 +214,31 @@ export function colorScale(
         const targetL =
             foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
 
-        if (index === 8 && !mix && Math.abs(baseColor.L - targetL) < 0.2) {
+        if (
+            index === 8 &&
+            !mix &&
+            (darkMode ? targetL - baseColor.L < 0.2 : baseColor.L - targetL < 0.2)
+        ) {
             // Original colour is close enough to target, so let's use the original colour as step 9.
             result.push(hex);
             continue;
         }
 
-        const chromaRatio = index === 8 || index === 9 ? 1 : index * 0.05;
+        const chromaRatio = (() => {
+            switch (index) {
+                // Step 9 and 10 have max chroma, meaning they are fully saturated.
+                case 8:
+                case 9:
+                    return 1;
+                // Step 11 and 12 have a reduced chroma
+                case 10:
+                    return 0.4;
+                case 11:
+                    return 0.1;
+                default:
+                    return index * 0.05;
+            }
+        })();
 
         const shade = {
             L: targetL, // Blend lightness
diff --git a/packages/fonts/.gitignore b/packages/fonts/.gitignore
new file mode 100644
index 0000000000..253839862f
--- /dev/null
+++ b/packages/fonts/.gitignore
@@ -0,0 +1,2 @@
+dist/
+src/data/*.json
diff --git a/packages/fonts/README.md b/packages/fonts/README.md
new file mode 100644
index 0000000000..fef253f27f
--- /dev/null
+++ b/packages/fonts/README.md
@@ -0,0 +1,3 @@
+# `@gitbook/fonts`
+
+Utilities to lookup default fonts supported by GitBook.
diff --git a/packages/fonts/bin/generate.ts b/packages/fonts/bin/generate.ts
new file mode 100644
index 0000000000..d4326d615c
--- /dev/null
+++ b/packages/fonts/bin/generate.ts
@@ -0,0 +1,91 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+
+import { APIv2 } from 'google-font-metadata';
+
+import { CustomizationDefaultFont } from '@gitbook/api';
+
+import type { FontDefinitions } from '../src/types';
+
+const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
+    [CustomizationDefaultFont.Inter]: 'inter',
+    [CustomizationDefaultFont.FiraSans]: 'fira-sans-extra-condensed',
+    [CustomizationDefaultFont.IBMPlexSerif]: 'ibm-plex-serif',
+    [CustomizationDefaultFont.Lato]: 'lato',
+    [CustomizationDefaultFont.Merriweather]: 'merriweather',
+    [CustomizationDefaultFont.NotoSans]: 'noto-sans',
+    [CustomizationDefaultFont.OpenSans]: 'open-sans',
+    [CustomizationDefaultFont.Overpass]: 'overpass',
+    [CustomizationDefaultFont.Poppins]: 'poppins',
+    [CustomizationDefaultFont.Raleway]: 'raleway',
+    [CustomizationDefaultFont.Roboto]: 'roboto',
+    [CustomizationDefaultFont.RobotoSlab]: 'roboto-slab',
+    [CustomizationDefaultFont.SourceSansPro]: 'source-sans-3',
+    [CustomizationDefaultFont.Ubuntu]: 'ubuntu',
+    [CustomizationDefaultFont.ABCFavorit]: 'inter',
+};
+
+/**
+ * Scripts to generate the list of all icons.
+ */
+async function main() {
+    // @ts-expect-error - we build the object
+    const output: FontDefinitions = {};
+
+    for (const font of Object.values(CustomizationDefaultFont)) {
+        const googleFontName = googleFontsMap[font];
+        const fontMetadata = APIv2[googleFontName.toLowerCase()];
+        if (!fontMetadata) {
+            throw new Error(`Font ${googleFontName} not found`);
+        }
+
+        output[font] = {
+            font: googleFontName,
+            unicodeRange: fontMetadata.unicodeRange,
+            variants: {
+                '400': {},
+                '700': {},
+            },
+        };
+
+        Object.keys(output[font].variants).forEach((weight) => {
+            const variants = fontMetadata.variants[weight];
+            const normalVariant = variants.normal;
+            if (!normalVariant) {
+                throw new Error(`Font ${googleFontName} has no normal variant`);
+            }
+
+            output[font].variants[weight] = {};
+            Object.entries(normalVariant).forEach(([script, url]) => {
+                output[font].variants[weight][script] = url.url.woff;
+            });
+        });
+    }
+
+    await writeDataFile('fonts', JSON.stringify(output, null, 2));
+}
+
+/**
+ * We write both in dist and src as the build process might have happen already
+ * and tsc doesn't copy the files.
+ */
+async function writeDataFile(name, content) {
+    const srcData = path.resolve(__dirname, '../src/data');
+    const distData = path.resolve(__dirname, '../dist/data');
+
+    // Ensure the directories exists
+    await Promise.all([
+        fs.mkdir(srcData, { recursive: true }),
+        fs.mkdir(distData, { recursive: true }),
+    ]);
+
+    await Promise.all([
+        fs.writeFile(path.resolve(srcData, `${name}.json`), content),
+        fs.writeFile(path.resolve(distData, `${name}.json`), content),
+    ]);
+}
+
+main().catch((error) => {
+    console.error(`Error generating icons list: ${error}`);
+    process.exit(1);
+});
diff --git a/packages/fonts/package.json b/packages/fonts/package.json
new file mode 100644
index 0000000000..dbfb2d7ca0
--- /dev/null
+++ b/packages/fonts/package.json
@@ -0,0 +1,31 @@
+{
+    "name": "@gitbook/fonts",
+    "type": "module",
+    "exports": {
+        ".": {
+            "types": "./dist/index.d.ts",
+            "development": "./src/index.ts",
+            "default": "./dist/index.js"
+        }
+    },
+    "version": "0.0.0",
+    "dependencies": {
+        "@gitbook/api": "catalog:"
+    },
+    "devDependencies": {
+        "google-font-metadata": "^6.0.3",
+        "typescript": "^5.5.3"
+    },
+    "scripts": {
+        "generate": "bun ./bin/generate.js",
+        "build": "tsc --project tsconfig.build.json",
+        "typecheck": "tsc --noEmit",
+        "dev": "tsc -w",
+        "clean": "rm -rf ./dist && rm -rf ./src/data",
+        "unit": "bun test"
+    },
+    "files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
+    "engines": {
+        "node": ">=20.0.0"
+    }
+}
diff --git a/packages/fonts/src/__snapshots__/getDefaultFont.test.ts.snap b/packages/fonts/src/__snapshots__/getDefaultFont.test.ts.snap
new file mode 100644
index 0000000000..89184be22c
--- /dev/null
+++ b/packages/fonts/src/__snapshots__/getDefaultFont.test.ts.snap
@@ -0,0 +1,57 @@
+// Bun Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`getDefaultFont should return correct object for Latin text 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
+}
+`;
+
+exports[`getDefaultFont should return correct object for Cyrillic text 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
+}
+`;
+
+exports[`getDefaultFont should return correct object for Greek text 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
+}
+`;
+
+exports[`getDefaultFont should handle mixed script text 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
+}
+`;
+
+exports[`getDefaultFont should handle different font weights: regular 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
+}
+`;
+
+exports[`getDefaultFont should handle different font weights: bold 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
+}
+`;
+
+exports[`getDefaultFont should handle different fonts: inter 1`] = `
+{
+  "font": "Inter",
+  "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
+}
+`;
+
+exports[`getDefaultFont should handle different fonts: roboto 1`] = `
+{
+  "font": "Roboto",
+  "url": "https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxMKTU1Kg.woff",
+}
+`;
diff --git a/packages/fonts/src/fonts.ts b/packages/fonts/src/fonts.ts
new file mode 100644
index 0000000000..c9544efb19
--- /dev/null
+++ b/packages/fonts/src/fonts.ts
@@ -0,0 +1,5 @@
+import type { FontDefinitions } from './types';
+
+import rawFonts from './data/fonts.json' with { type: 'json' };
+
+export const fonts: FontDefinitions = rawFonts;
diff --git a/packages/fonts/src/getDefaultFont.test.ts b/packages/fonts/src/getDefaultFont.test.ts
new file mode 100644
index 0000000000..58afbeac93
--- /dev/null
+++ b/packages/fonts/src/getDefaultFont.test.ts
@@ -0,0 +1,119 @@
+import { describe, expect, it } from 'bun:test';
+import { CustomizationDefaultFont } from '@gitbook/api';
+import { getDefaultFont } from './getDefaultFont';
+
+describe('getDefaultFont', () => {
+    it('should return null for invalid font', () => {
+        const result = getDefaultFont({
+            font: 'invalid-font' as CustomizationDefaultFont,
+            text: 'Hello',
+            weight: 400,
+        });
+        expect(result).toBeNull();
+    });
+
+    it('should return null for invalid weight', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello',
+            weight: 999 as any,
+        });
+        expect(result).toBeNull();
+    });
+
+    it('should return null for text not supported by any script', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: '😀', // Emoji not supported by Inter
+            weight: 400,
+        });
+        expect(result).toBeNull();
+    });
+
+    it('should return correct object for Latin text', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello World',
+            weight: 400,
+        });
+        expect(result).not.toBeNull();
+        expect(result?.font).toBe(CustomizationDefaultFont.Inter);
+        expect(result).toMatchSnapshot();
+    });
+
+    it('should return correct object for Cyrillic text', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Привет мир',
+            weight: 400,
+        });
+        expect(result).not.toBeNull();
+        expect(result?.font).toBe(CustomizationDefaultFont.Inter);
+        expect(result).toMatchSnapshot();
+    });
+
+    it('should return correct object for Greek text', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Γεια σας',
+            weight: 400,
+        });
+        expect(result).not.toBeNull();
+        expect(result?.font).toBe(CustomizationDefaultFont.Inter);
+        expect(result).toMatchSnapshot();
+    });
+
+    it('should handle mixed script text', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello Привет',
+            weight: 400,
+        });
+        expect(result).not.toBeNull();
+        expect(result?.font).toBe(CustomizationDefaultFont.Inter);
+        expect(result).toMatchSnapshot();
+    });
+
+    it('should handle different font weights', () => {
+        const regular = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello',
+            weight: 400,
+        });
+        const bold = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello',
+            weight: 700,
+        });
+        expect(regular).not.toBeNull();
+        expect(bold).not.toBeNull();
+        expect(regular).toMatchSnapshot('regular');
+        expect(bold).toMatchSnapshot('bold');
+    });
+
+    it('should handle empty string', () => {
+        const result = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: '',
+            weight: 400,
+        });
+        expect(result).toBeNull();
+    });
+
+    it('should handle different fonts', () => {
+        const inter = getDefaultFont({
+            font: CustomizationDefaultFont.Inter,
+            text: 'Hello',
+            weight: 400,
+        });
+        const roboto = getDefaultFont({
+            font: CustomizationDefaultFont.Roboto,
+            text: 'Hello',
+            weight: 400,
+        });
+        expect(inter).not.toBeNull();
+        expect(roboto).not.toBeNull();
+        expect(inter).toMatchSnapshot('inter');
+        expect(roboto).toMatchSnapshot('roboto');
+    });
+});
diff --git a/packages/fonts/src/getDefaultFont.ts b/packages/fonts/src/getDefaultFont.ts
new file mode 100644
index 0000000000..ea18e8bba2
--- /dev/null
+++ b/packages/fonts/src/getDefaultFont.ts
@@ -0,0 +1,112 @@
+import type { CustomizationDefaultFont } from '@gitbook/api';
+import { fonts } from './fonts';
+import type { FontWeight } from './types';
+
+/**
+ * Get the URL to load a font for a text.
+ */
+export function getDefaultFont(input: {
+    /**
+     * GitBook font to use.
+     */
+    font: CustomizationDefaultFont;
+
+    /**
+     * Text to display with the font.
+     */
+    text: string;
+
+    /**
+     * Font weight to use.
+     */
+    weight: FontWeight;
+}): { font: string; url: string } | null {
+    if (!input.text.trim()) {
+        return null;
+    }
+
+    const fontDefinition = fonts[input.font];
+    if (!fontDefinition) {
+        return null;
+    }
+
+    const variant = fontDefinition.variants[`${input.weight}`];
+    if (!variant) {
+        return null;
+    }
+
+    const script = getBestUnicodeRange(input.text, fontDefinition.unicodeRange);
+    if (!script) {
+        return null;
+    }
+
+    return variant[script]
+        ? {
+              font: input.font,
+              url: variant[script],
+          }
+        : null;
+}
+
+/**
+ * Determine which named @font-face unicode-range covers
+ * the greatest share of the characters in `text`.
+ *
+ * @param text   The text you want to inspect.
+ * @param ranges An object whose keys are range names and whose
+ *               values are CSS-style comma-separated unicode-range
+ *               declarations (e.g. "U+0370-03FF,U+1F00-1FFF").
+ * @returns      The key of the best-matching range, or `null`
+ *               when nothing matches at all.
+ */
+function getBestUnicodeRange(text: string, ranges: Record<string, string>): string | null {
+    // ---------- helper: parse "U+XXXX" or "U+XXXX-YYYY" ----------
+    const parseOne = (token: string): [number, number] | null => {
+        token = token.trim().toUpperCase();
+        if (!token.startsWith('U+')) return null;
+
+        const body = token.slice(2); // drop "U+"
+        const [startHex, endHex] = body.split('-');
+        const start = Number.parseInt(startHex, 16);
+        const end = endHex ? Number.parseInt(endHex, 16) : start;
+
+        if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
+        return [start, end];
+    };
+
+    // ---------- helper: build lookup table ----------
+    const parsed: Record<string, [number, number][]> = {};
+    for (const [label, list] of Object.entries(ranges)) {
+        parsed[label] = list
+            .split(',')
+            .map(parseOne)
+            .filter((x): x is [number, number] => x !== null);
+    }
+
+    // ---------- tally code-point hits ----------
+    const hits: Record<string, number> = Object.fromEntries(Object.keys(parsed).map((k) => [k, 0]));
+
+    for (let i = 0; i < text.length; ) {
+        const cp = text.codePointAt(i)!;
+        i += cp > 0xffff ? 2 : 1; // advance by 1 UTF-16 code-unit (or 2 for surrogates)
+
+        for (const [label, rangesArr] of Object.entries(parsed)) {
+            if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) {
+                hits[label]++;
+            }
+        }
+    }
+
+    // ---------- choose the "best" ----------
+    let winner: string | null = null;
+    let maxCount = 0;
+
+    for (const [label, count] of Object.entries(hits)) {
+        if (count > maxCount) {
+            maxCount = count;
+            winner = label;
+        }
+    }
+
+    return maxCount > 0 ? winner : null;
+}
diff --git a/packages/fonts/src/index.ts b/packages/fonts/src/index.ts
new file mode 100644
index 0000000000..fbb3584899
--- /dev/null
+++ b/packages/fonts/src/index.ts
@@ -0,0 +1,2 @@
+export * from './getDefaultFont';
+export * from './types';
diff --git a/packages/fonts/src/types.ts b/packages/fonts/src/types.ts
new file mode 100644
index 0000000000..899b32c49f
--- /dev/null
+++ b/packages/fonts/src/types.ts
@@ -0,0 +1,17 @@
+import type { CustomizationDefaultFont } from '@gitbook/api';
+
+export type FontWeight = 400 | 700;
+
+export type FontDefinition = {
+    font: string;
+    unicodeRange: {
+        [script: string]: string;
+    };
+    variants: {
+        [weight in string]: {
+            [script: string]: string;
+        };
+    };
+};
+
+export type FontDefinitions = { [fontName in CustomizationDefaultFont]: FontDefinition };
diff --git a/packages/fonts/tsconfig.build.json b/packages/fonts/tsconfig.build.json
new file mode 100644
index 0000000000..e4828bc1f6
--- /dev/null
+++ b/packages/fonts/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+    "extends": "./tsconfig.json",
+    "exclude": ["node_modules", "src/**/*.test.ts"]
+}
diff --git a/packages/fonts/tsconfig.json b/packages/fonts/tsconfig.json
new file mode 100644
index 0000000000..2b3fe87c5f
--- /dev/null
+++ b/packages/fonts/tsconfig.json
@@ -0,0 +1,24 @@
+{
+    "compilerOptions": {
+        "lib": ["es2023"],
+        "module": "ESNext",
+        "target": "es2022",
+        "strict": true,
+        "esModuleInterop": true,
+        "skipLibCheck": true,
+        "moduleResolution": "bundler",
+        "allowJs": true,
+        "noEmit": false,
+        "declaration": true,
+        "outDir": "dist",
+        "resolveJsonModule": true,
+        "isolatedModules": true,
+        "jsx": "react-jsx",
+        "incremental": true,
+        "types": [
+            "bun-types" // add Bun global
+        ]
+    },
+    "include": ["src/**/*.ts"],
+    "exclude": ["node_modules"]
+}
diff --git a/packages/fonts/turbo.json b/packages/fonts/turbo.json
new file mode 100644
index 0000000000..9097cda33c
--- /dev/null
+++ b/packages/fonts/turbo.json
@@ -0,0 +1,9 @@
+{
+    "extends": ["//"],
+    "tasks": {
+        "generate": {
+            "inputs": ["bin/**/*", "package.json"],
+            "outputs": ["src/data/*.json", "dist/data/*.json"]
+        }
+    }
+}
diff --git a/packages/gitbook-v2/.gitignore b/packages/gitbook-v2/.gitignore
index 84254823d2..a23d5b7565 100644
--- a/packages/gitbook-v2/.gitignore
+++ b/packages/gitbook-v2/.gitignore
@@ -6,6 +6,7 @@
 
 # cloudflare
 .open-next
+.wrangler
 
 # Symbolic links
 public
diff --git a/packages/gitbook-v2/CHANGELOG.md b/packages/gitbook-v2/CHANGELOG.md
index eb4e718a5b..8a4a4a0288 100644
--- a/packages/gitbook-v2/CHANGELOG.md
+++ b/packages/gitbook-v2/CHANGELOG.md
@@ -1,5 +1,24 @@
 # gitbook-v2
 
+## 0.3.0
+
+### Minor Changes
+
+- 3119066: Add support for reusable content across spaces.
+- 7d7806d: Pass SVG images through image resizing without resizing them to serve them from optimal host.
+
+### Patch Changes
+
+- 1c8d9fe: keep data cache in OpenNext between deployment
+- 778624a: Only resize images with supported extensions.
+- e6ddc0f: Fix URL in sitemap
+- 5e975ab: Fix code highlighting for HTTP
+- e15757d: Fix crash on Cloudflare by using latest stable version of Next.js instead of canary
+- 634e0b4: Improve error messages around undefined site sections.
+- 97b7c79: Increase logging around caching behaviour causing page crashes.
+- 3f29206: Update the regex for validating site redirect
+- dd043df: Revert investigation work around URL caches.
+
 ## 0.2.5
 
 ### Patch Changes
diff --git a/packages/gitbook-v2/next-env.d.ts b/packages/gitbook-v2/next-env.d.ts
index 1b3be0840f..3cd7048ed9 100644
--- a/packages/gitbook-v2/next-env.d.ts
+++ b/packages/gitbook-v2/next-env.d.ts
@@ -1,5 +1,6 @@
 /// <reference types="next" />
 /// <reference types="next/image-types/global" />
+/// <reference types="next/navigation-types/compat/navigation" />
 
 // NOTE: This file should not be edited
 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts
index 8f4d389da6..1872309119 100644
--- a/packages/gitbook-v2/open-next.config.ts
+++ b/packages/gitbook-v2/open-next.config.ts
@@ -1,18 +1,29 @@
-import { defineCloudflareConfig } from '@opennextjs/cloudflare';
-import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache';
-import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
-import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
-import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
+import type { OpenNextConfig } from '@opennextjs/cloudflare';
 
-export default defineCloudflareConfig({
-    incrementalCache: withRegionalCache(r2IncrementalCache, { mode: 'long-lived' }),
-    tagCache: doShardedTagCache({
-        baseShardSize: 12,
-        regionalCache: true,
-        shardReplication: {
-            numberOfSoftReplicas: 2,
-            numberOfHardReplicas: 1,
+export default {
+    default: {
+        override: {
+            wrapper: 'cloudflare-node',
+            converter: 'edge',
+            proxyExternalRequest: 'fetch',
+            queue: () => import('./openNext/queue/middleware').then((m) => m.default),
+            incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
+            tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
         },
-    }),
-    queue: doQueue,
-});
+    },
+    middleware: {
+        external: true,
+        override: {
+            wrapper: 'cloudflare-edge',
+            converter: 'edge',
+            proxyExternalRequest: 'fetch',
+            queue: () => import('./openNext/queue/middleware').then((m) => m.default),
+            incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
+            tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
+        },
+    },
+    dangerous: {
+        enableCacheInterception: true,
+    },
+    edgeExternals: ['node:crypto'],
+} satisfies OpenNextConfig;
diff --git a/packages/gitbook-v2/openNext/customWorkers/default.js b/packages/gitbook-v2/openNext/customWorkers/default.js
new file mode 100644
index 0000000000..535c218167
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/default.js
@@ -0,0 +1,36 @@
+import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
+
+import { DurableObject } from 'cloudflare:workers';
+
+// Only needed to run locally, in prod we'll use the one from do.js
+export class R2WriteBuffer extends DurableObject {
+    writePromise;
+
+    async write(cacheKey, value) {
+        // We are already writing to this key
+        if (this.writePromise) {
+            return;
+        }
+
+        this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
+        this.ctx.waitUntil(
+            this.writePromise.finally(() => {
+                this.writePromise = undefined;
+            })
+        );
+    }
+}
+
+export default {
+    async fetch(request, env, ctx) {
+        return runWithCloudflareRequestContext(request, env, ctx, async () => {
+            // We can't move the handler import to the top level, otherwise the runtime will not be properly initialized
+            const { handler } = await import(
+                '../../.open-next/server-functions/default/handler.mjs'
+            );
+
+            // - `Request`s are handled by the Next server
+            return handler(request, env, ctx);
+        });
+    },
+};
diff --git a/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
new file mode 100644
index 0000000000..e036db1c44
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
@@ -0,0 +1,163 @@
+{
+    "main": "default.js",
+    "name": "gitbook-open-v2-server",
+    "compatibility_date": "2025-04-14",
+    "compatibility_flags": [
+        "nodejs_compat",
+        "allow_importable_env",
+        "global_fetch_strictly_public"
+    ],
+    "observability": {
+        "enabled": true
+    },
+    "vars": {
+        "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
+    },
+    "env": {
+        "dev": {
+            "vars": {
+                "STAGE": "dev"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-preview"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-server-dev"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["R2WriteBuffer"]
+                }
+            ]
+        },
+        "preview": {
+            "vars": {
+                "STAGE": "preview",
+                // Just as a test for the preview environment to check that everything works
+                "NEXT_PRIVATE_DEBUG_CACHE": "true"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-preview"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-server-preview"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    }
+                ]
+            }
+        },
+        "staging": {
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-staging"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-server-staging"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-staging"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-do-staging"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-staging"
+                    }
+                ]
+            },
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-staging-tail"
+                }
+            ]
+        },
+        "production": {
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-production"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-server-production"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-production"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-do-production"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-production"
+                    }
+                ]
+            },
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-prod-tail"
+                }
+            ]
+        }
+    }
+}
diff --git a/packages/gitbook-v2/openNext/customWorkers/do.js b/packages/gitbook-v2/openNext/customWorkers/do.js
new file mode 100644
index 0000000000..04f3cf3bec
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/do.js
@@ -0,0 +1,38 @@
+// This worker only purposes it to host the different DO that we will need in the other workers.
+import { DurableObject } from 'cloudflare:workers';
+
+// `use cache` could cause multiple writes to the same key to happen concurrently, there is a limit of 1 write per key/second
+// so we need to buffer writes to the R2 bucket to avoid hitting this limit.
+export class R2WriteBuffer extends DurableObject {
+    writePromise;
+
+    async write(cacheKey, value) {
+        // We are already writing to this key
+        if (this.writePromise) {
+            return;
+        }
+
+        this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
+        this.ctx.waitUntil(
+            this.writePromise.finally(() => {
+                this.writePromise = undefined;
+            })
+        );
+    }
+}
+
+export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
+
+export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
+
+export default {
+    async fetch() {
+        // This worker does not handle any requests, it only provides Durable Objects
+        return new Response('This worker is not meant to handle requests directly', {
+            status: 400,
+            headers: {
+                'Content-Type': 'text/plain',
+            },
+        });
+    },
+};
diff --git a/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
new file mode 100644
index 0000000000..e239202bef
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
@@ -0,0 +1,145 @@
+{
+    "main": "do.js",
+    "name": "gitbook-open-v2-do",
+    "compatibility_date": "2025-04-14",
+    "compatibility_flags": [
+        "nodejs_compat",
+        "allow_importable_env",
+        "global_fetch_strictly_public"
+    ],
+    "observability": {
+        "enabled": true
+    },
+    "env": {
+        "preview": {
+            "vars": {
+                "STAGE": "preview",
+                "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-preview"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-preview"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache"
+                    },
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
+                }
+            ]
+        },
+        "staging": {
+            "vars": {
+                "STAGE": "staging",
+                "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-staging"
+                }
+            ],
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-staging-tail"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-staging"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache"
+                    },
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
+                }
+            ]
+        },
+        "production": {
+            "vars": {
+                "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
+                "STAGE": "production"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-production"
+                }
+            ],
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-prod-tail"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-production"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache"
+                    },
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
+                }
+            ]
+        }
+    }
+}
diff --git a/packages/gitbook-v2/openNext/customWorkers/middleware.js b/packages/gitbook-v2/openNext/customWorkers/middleware.js
new file mode 100644
index 0000000000..78a84a9760
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/middleware.js
@@ -0,0 +1,42 @@
+import { WorkerEntrypoint } from 'cloudflare:workers';
+import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
+
+import { handler as middlewareHandler } from '../../.open-next/middleware/handler.mjs';
+
+export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
+
+export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
+
+export default class extends WorkerEntrypoint {
+    async fetch(request) {
+        return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
+            // - `Request`s are handled by the Next server
+            const reqOrResp = await middlewareHandler(request, this.env, this.ctx);
+            if (reqOrResp instanceof Response) {
+                return reqOrResp;
+            }
+
+            if (this.env.STAGE !== 'preview') {
+                // https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/#version-affinity
+                reqOrResp.headers.set(
+                    'Cloudflare-Workers-Version-Overrides',
+                    `gitbook-open-v2-${this.env.STAGE}="${this.env.WORKER_VERSION_ID}"`
+                );
+                return this.env.DEFAULT_WORKER?.fetch(reqOrResp, {
+                    cf: {
+                        cacheEverything: false,
+                    },
+                });
+            }
+            // If we are in preview mode, we need to send the request to the preview URL
+            const modifiedUrl = new URL(reqOrResp.url);
+            modifiedUrl.hostname = this.env.PREVIEW_HOSTNAME;
+            const nextRequest = new Request(modifiedUrl, reqOrResp);
+            return fetch(nextRequest, {
+                cf: {
+                    cacheEverything: false,
+                },
+            });
+        });
+    }
+}
diff --git a/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
new file mode 100644
index 0000000000..09e48afcc0
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
@@ -0,0 +1,216 @@
+{
+    "main": "middleware.js",
+    "name": "gitbook-open-v2",
+    "compatibility_date": "2025-04-14",
+    "compatibility_flags": [
+        "nodejs_compat",
+        "allow_importable_env",
+        "global_fetch_strictly_public"
+    ],
+    "assets": {
+        "directory": "../../.open-next/assets",
+        "binding": "ASSETS"
+    },
+    "observability": {
+        "enabled": true
+    },
+    "vars": {
+        "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
+    },
+    "env": {
+        "dev": {
+            "vars": {
+                "STAGE": "dev",
+                "NEXT_PRIVATE_DEBUG_CACHE": "true"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-preview"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-dev"
+                },
+                {
+                    "binding": "DEFAULT_WORKER",
+                    "service": "gitbook-open-v2-server-dev"
+                }
+            ]
+        },
+        "preview": {
+            "vars": {
+                "STAGE": "preview",
+                "PREVIEW_HOSTNAME": "TO_REPLACE",
+                "WORKER_VERSION_ID": "TO_REPLACE"
+            },
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-preview"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-preview"
+                },
+                {
+                    "binding": "DEFAULT_WORKER",
+                    "service": "gitbook-open-v2-server-preview"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-preview"
+                    }
+                ]
+            }
+        },
+        "staging": {
+            "vars": {
+                "STAGE": "staging",
+                "WORKER_VERSION_ID": "TO_REPLACE"
+            },
+            "routes": [
+                {
+                    "pattern": "open-2c.gitbook-staging.com/*",
+                    "zone_name": "gitbook-staging.com"
+                },
+                {
+                    "pattern": "static-2c.gitbook-staging.com/*",
+                    "zone_name": "gitbook-staging.com"
+                }
+            ],
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-staging"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-staging"
+                },
+                {
+                    "binding": "DEFAULT_WORKER",
+                    "service": "gitbook-open-v2-server-staging"
+                }
+            ],
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-staging-tail"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-staging"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-do-staging"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-staging"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
+                }
+            ]
+        },
+        "production": {
+            "vars": {
+                // This is a bit misleading, but it means that we can have 500 concurrent revalidations
+                // This means that we'll have up to 100 durable objects instance running at the same time
+                "MAX_REVALIDATE_CONCURRENCY": "100",
+                // Temporary variable to find the issue once deployed
+                // TODO: remove this once the issue is fixed
+                "DEBUG_CLOUDFLARE": "true",
+                "WORKER_VERSION_ID": "TO_REPLACE",
+                "STAGE": "production"
+            },
+            "routes": [
+                {
+                    "pattern": "open-2c.gitbook.com/*",
+                    "zone_name": "gitbook.com"
+                },
+                {
+                    "pattern": "static-2c.gitbook.com/*",
+                    "zone_name": "gitbook.com"
+                }
+            ],
+            "r2_buckets": [
+                {
+                    "binding": "NEXT_INC_CACHE_R2_BUCKET",
+                    "bucket_name": "gitbook-open-v2-cache-production"
+                }
+            ],
+            "services": [
+                {
+                    "binding": "WORKER_SELF_REFERENCE",
+                    "service": "gitbook-open-v2-production"
+                },
+                {
+                    "binding": "DEFAULT_WORKER",
+                    "service": "gitbook-open-v2-server-production"
+                }
+            ],
+            "tail_consumers": [
+                {
+                    "service": "gitbook-x-prod-tail"
+                }
+            ],
+            "durable_objects": {
+                "bindings": [
+                    {
+                        "name": "WRITE_BUFFER",
+                        "class_name": "R2WriteBuffer",
+                        "script_name": "gitbook-open-v2-do-production"
+                    },
+                    {
+                        "name": "NEXT_TAG_CACHE_DO_SHARDED",
+                        "class_name": "DOShardedTagCache",
+                        "script_name": "gitbook-open-v2-do-production"
+                    },
+                    {
+                        "name": "NEXT_CACHE_DO_QUEUE",
+                        "class_name": "DOQueueHandler",
+                        "script_name": "gitbook-open-v2-do-production"
+                    }
+                ]
+            },
+            "migrations": [
+                {
+                    "tag": "v1",
+                    "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
+                }
+            ]
+        }
+    }
+}
diff --git a/packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts b/packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts
new file mode 100644
index 0000000000..0fdbf6cc70
--- /dev/null
+++ b/packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts
@@ -0,0 +1,26 @@
+// In this script, we use the args from the cli to update the PREVIEW_URL vars in the wrangler config file for the middleware
+import fs from 'node:fs';
+import path from 'node:path';
+
+const wranglerConfigPath = path.join(__dirname, '../middlewareWrangler.jsonc');
+
+const file = fs.readFileSync(wranglerConfigPath, 'utf-8');
+
+const args = process.argv.slice(2);
+// The versionId is in the format xxx-xxx-xxx-xxx, we need the first part to reconstruct the preview URL
+const versionId = args[0];
+
+// The preview URL is in the format https://<versionId>-gitbook-open-v2-server-preview.gitbook.workers.dev
+const previewHostname = `${versionId.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
+
+let updatedFile = file.replace(
+    /"PREVIEW_HOSTNAME": "TO_REPLACE"/,
+    `"PREVIEW_HOSTNAME": "${previewHostname}"`
+);
+
+updatedFile = updatedFile.replaceAll(
+    /"WORKER_VERSION_ID": "TO_REPLACE"/g,
+    `"WORKER_VERSION_ID": "${versionId}"`
+);
+
+fs.writeFileSync(wranglerConfigPath, updatedFile);
diff --git a/packages/gitbook-v2/openNext/incrementalCache.ts b/packages/gitbook-v2/openNext/incrementalCache.ts
new file mode 100644
index 0000000000..8e4ca98635
--- /dev/null
+++ b/packages/gitbook-v2/openNext/incrementalCache.ts
@@ -0,0 +1,217 @@
+import { createHash } from 'node:crypto';
+
+import { trace } from '@/lib/tracing';
+import type {
+    CacheEntryType,
+    CacheValue,
+    IncrementalCache,
+    WithLastModified,
+} from '@opennextjs/aws/types/overrides.js';
+import { getCloudflareContext } from '@opennextjs/cloudflare';
+
+import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
+
+export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
+export const DEFAULT_PREFIX = 'incremental-cache';
+
+export type KeyOptions = {
+    cacheType?: CacheEntryType;
+};
+
+/**
+ *
+ * It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace
+ * the cache operations. It also integrates both R2 and Cache API in a single class.
+ * Having our own, will allow us to customize it in the future if needed.
+ */
+class GitbookIncrementalCache implements IncrementalCache {
+    name = 'GitbookIncrementalCache';
+
+    protected localCache: Cache | undefined;
+
+    async get<CacheType extends CacheEntryType = 'cache'>(
+        key: string,
+        cacheType?: CacheType
+    ): Promise<WithLastModified<CacheValue<CacheType>> | null> {
+        const cacheKey = this.getR2Key(key, cacheType);
+        return trace(
+            {
+                operation: 'openNextIncrementalCacheGet',
+                name: cacheKey,
+            },
+            async (span) => {
+                span.setAttribute('cacheType', cacheType ?? 'cache');
+                const r2 = getCloudflareContext().env[BINDING_NAME];
+                const localCache = await this.getCacheInstance();
+                if (!r2) throw new Error('No R2 bucket');
+                try {
+                    // Check local cache first if available
+                    const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
+                    if (localCacheEntry) {
+                        span.setAttribute('cacheHit', 'local');
+                        const result = (await localCacheEntry.json()) as WithLastModified<
+                            CacheValue<CacheType>
+                        >;
+                        return this.returnNullOn404(result);
+                    }
+
+                    const r2Object = await r2.get(cacheKey);
+                    if (!r2Object) return null;
+
+                    span.setAttribute('cacheHit', 'r2');
+                    return this.returnNullOn404({
+                        value: await r2Object.json(),
+                        lastModified: r2Object.uploaded.getTime(),
+                    });
+                } catch (e) {
+                    console.error('Failed to get from cache', e);
+                    return null;
+                }
+            }
+        );
+    }
+
+    //TODO: This is a workaround to handle 404 responses in the cache.
+    // It should be handled by OpenNext cache interception directly. This should be removed once OpenNext cache interception is fixed.
+    returnNullOn404<CacheType extends CacheEntryType = 'cache'>(
+        cacheEntry: WithLastModified<CacheValue<CacheType>> | null
+    ): WithLastModified<CacheValue<CacheType>> | null {
+        if (!cacheEntry?.value) return null;
+        if ('meta' in cacheEntry.value && cacheEntry.value.meta?.status === 404) {
+            return null;
+        }
+        return cacheEntry;
+    }
+
+    async set<CacheType extends CacheEntryType = 'cache'>(
+        key: string,
+        value: CacheValue<CacheType>,
+        cacheType?: CacheType
+    ): Promise<void> {
+        const cacheKey = this.getR2Key(key, cacheType);
+        return trace(
+            {
+                operation: 'openNextIncrementalCacheSet',
+                name: cacheKey,
+            },
+            async (span) => {
+                span.setAttribute('cacheType', cacheType ?? 'cache');
+                const localCache = await this.getCacheInstance();
+
+                try {
+                    await this.writeToR2(cacheKey, JSON.stringify(value));
+
+                    //TODO: Check if there is any places where we don't have tags
+                    // Ideally we should always have tags, but in case we don't, we need to decide how to handle it
+                    // For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment
+                    const tags = this.getTagsFromCacheEntry(value) ?? [
+                        `build_id/${process.env.NEXT_BUILD_ID}`,
+                    ];
+
+                    // We consider R2 as the source of truth, so we update the local cache
+                    // only after a successful R2 write
+                    await localCache.put(
+                        this.getCacheUrlKey(cacheKey),
+                        new Response(
+                            JSON.stringify({
+                                value,
+                                // Note: `Date.now()` returns the time of the last IO rather than the actual time.
+                                //       See https://developers.cloudflare.com/workers/reference/security-model/
+                                lastModified: Date.now(),
+                            }),
+                            {
+                                headers: {
+                                    // Cache-Control default to 30 minutes, will be overridden by `revalidate`
+                                    // In theory we should always get the `revalidate` value
+                                    'cache-control': `max-age=${value.revalidate ?? 60 * 30}`,
+                                    'cache-tag': tags.join(','),
+                                },
+                            }
+                        )
+                    );
+                } catch (e) {
+                    console.error('Failed to set to cache', e);
+                }
+            }
+        );
+    }
+
+    async delete(key: string): Promise<void> {
+        const cacheKey = this.getR2Key(key);
+        return trace(
+            {
+                operation: 'openNextIncrementalCacheDelete',
+                name: cacheKey,
+            },
+            async () => {
+                const r2 = getCloudflareContext().env[BINDING_NAME];
+                const localCache = await this.getCacheInstance();
+                if (!r2) throw new Error('No R2 bucket');
+
+                try {
+                    await r2.delete(cacheKey);
+
+                    // Here again R2 is the source of truth, so we delete from local cache first
+                    await localCache.delete(this.getCacheUrlKey(cacheKey));
+                } catch (e) {
+                    console.error('Failed to delete from cache', e);
+                }
+            }
+        );
+    }
+
+    async writeToR2(key: string, value: string): Promise<void> {
+        const env = getCloudflareContext().env as {
+            WRITE_BUFFER: DurableObjectNamespace<
+                Rpc.DurableObjectBranded & {
+                    write: (key: string, value: string) => Promise<void>;
+                }
+            >;
+        };
+        const id = env.WRITE_BUFFER.idFromName(key);
+
+        // A stub is a client used to invoke methods on the Durable Object
+        const stub = env.WRITE_BUFFER.get(id);
+
+        await stub.write(key, value);
+    }
+
+    async getCacheInstance(): Promise<Cache> {
+        if (this.localCache) return this.localCache;
+        this.localCache = await caches.open('incremental-cache');
+        return this.localCache;
+    }
+
+    // Utility function to generate keys for R2/Cache API
+    getR2Key(key: string, cacheType: CacheEntryType = 'cache'): string {
+        const hash = createHash('sha256').update(key).digest('hex');
+        return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? process.env?.NEXT_BUILD_ID : 'dataCache'}/${hash}.${cacheType}`.replace(
+            /\/+/g,
+            '/'
+        );
+    }
+
+    getCacheUrlKey(cacheKey: string): string {
+        return `http://cache.local/${cacheKey}`;
+    }
+
+    getTagsFromCacheEntry<CacheType extends CacheEntryType>(
+        entry: CacheValue<CacheType>
+    ): string[] | undefined {
+        if ('tags' in entry && entry.tags) {
+            return entry.tags;
+        }
+
+        if ('meta' in entry && entry.meta && 'headers' in entry.meta && entry.meta.headers) {
+            const rawTags = entry.meta.headers['x-next-cache-tags'];
+            if (typeof rawTags === 'string') {
+                return rawTags.split(',');
+            }
+        }
+        if ('value' in entry) {
+            return entry.tags;
+        }
+    }
+}
+
+export default new GitbookIncrementalCache();
diff --git a/packages/gitbook-v2/openNext/queue/middleware.ts b/packages/gitbook-v2/openNext/queue/middleware.ts
new file mode 100644
index 0000000000..5ab486a975
--- /dev/null
+++ b/packages/gitbook-v2/openNext/queue/middleware.ts
@@ -0,0 +1,14 @@
+import { trace } from '@/lib/tracing';
+import type { Queue } from '@opennextjs/aws/types/overrides.js';
+import { getCloudflareContext } from '@opennextjs/cloudflare';
+import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
+
+export default {
+    name: 'GitbookISRQueue',
+    send: async (msg) => {
+        return trace({ operation: 'gitbookISRQueueSend', name: msg.MessageBody.url }, async () => {
+            const { ctx } = getCloudflareContext();
+            ctx.waitUntil(doQueue.send(msg));
+        });
+    },
+} satisfies Queue;
diff --git a/packages/gitbook-v2/openNext/queue/server.ts b/packages/gitbook-v2/openNext/queue/server.ts
new file mode 100644
index 0000000000..9a5b3b689b
--- /dev/null
+++ b/packages/gitbook-v2/openNext/queue/server.ts
@@ -0,0 +1,9 @@
+import type { Queue } from '@opennextjs/aws/types/overrides.js';
+
+export default {
+    name: 'GitbookISRQueue',
+    send: async (msg) => {
+        // We should never reach this point in the server. If that's the case, we should log it.
+        console.warn('GitbookISRQueue: send called on server side, this should not happen.', msg);
+    },
+} satisfies Queue;
diff --git a/packages/gitbook-v2/openNext/tagCache/middleware.ts b/packages/gitbook-v2/openNext/tagCache/middleware.ts
new file mode 100644
index 0000000000..4f06d7896b
--- /dev/null
+++ b/packages/gitbook-v2/openNext/tagCache/middleware.ts
@@ -0,0 +1,81 @@
+import { trace } from '@/lib/tracing';
+import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js';
+import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
+import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
+
+const originalTagCache = doShardedTagCache({
+    baseShardSize: 12,
+    regionalCache: true,
+    // We can probably increase this value even further
+    regionalCacheTtlSec: 60,
+    shardReplication: {
+        numberOfSoftReplicas: 2,
+        numberOfHardReplicas: 1,
+        regionalReplication: {
+            defaultRegion: 'enam',
+        },
+    },
+});
+
+export default {
+    name: 'GitbookTagCache',
+    mode: 'nextMode',
+    getLastRevalidated: async (tags: string[]) => {
+        const tagsToCheck = tags.filter(softTagFilter);
+        if (tagsToCheck.length === 0) {
+            // If we reach here, it probably means that there is an issue that we'll need to address.
+            console.warn(
+                'getLastRevalidated - No valid tags to check for last revalidation, original tags:',
+                tags
+            );
+            return 0; // If no tags to check, return 0
+        }
+        return trace(
+            {
+                operation: 'gitbookTagCacheGetLastRevalidated',
+                name: tagsToCheck.join(', '),
+            },
+            async () => {
+                return await originalTagCache.getLastRevalidated(tagsToCheck);
+            }
+        );
+    },
+    hasBeenRevalidated: async (tags: string[], lastModified?: number) => {
+        const tagsToCheck = tags.filter(softTagFilter);
+        if (tagsToCheck.length === 0) {
+            // If we reach here, it probably means that there is an issue that we'll need to address.
+            console.warn(
+                'hasBeenRevalidated - No valid tags to check for revalidation, original tags:',
+                tags
+            );
+            return false; // If no tags to check, return false
+        }
+        return trace(
+            {
+                operation: 'gitbookTagCacheHasBeenRevalidated',
+                name: tagsToCheck.join(', '),
+            },
+            async () => {
+                const result = await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified);
+                return result;
+            }
+        );
+    },
+    writeTags: async (tags: string[]) => {
+        return trace(
+            {
+                operation: 'gitbookTagCacheWriteTags',
+                name: tags.join(', '),
+            },
+            async () => {
+                const tagsToWrite = tags.filter(softTagFilter);
+                if (tagsToWrite.length === 0) {
+                    console.warn('writeTags - No valid tags to write');
+                    return; // If no tags to write, exit early
+                }
+                // Write only the filtered tags
+                await originalTagCache.writeTags(tagsToWrite);
+            }
+        );
+    },
+} satisfies NextModeTagCache;
diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json
index 50cf96a0d7..cfa6e71b91 100644
--- a/packages/gitbook-v2/package.json
+++ b/packages/gitbook-v2/package.json
@@ -1,23 +1,24 @@
 {
     "name": "gitbook-v2",
-    "version": "0.2.5",
+    "version": "0.3.0",
     "private": true,
     "dependencies": {
-        "next": "canary",
-        "react": "^19.0.0",
-        "react-dom": "^19.0.0",
-        "@gitbook/api": "*",
+        "@gitbook/api": "catalog:",
         "@gitbook/cache-tags": "workspace:*",
+        "@opennextjs/cloudflare": "1.2.1",
         "@sindresorhus/fnv1a": "^3.1.0",
+        "assert-never": "^1.2.1",
+        "jwt-decode": "^4.0.0",
+        "next": "^15.3.2",
+        "react": "^19.0.0",
+        "react-dom": "^19.0.0",
+        "rison": "^0.1.1",
         "server-only": "^0.0.1",
         "warn-once": "^0.1.1",
-        "rison": "^0.1.1",
-        "jwt-decode": "^4.0.0",
-        "assert-never": "^1.2.1"
+        "object-identity": "^0.1.2"
     },
     "devDependencies": {
         "gitbook": "*",
-        "@opennextjs/cloudflare": "^1.0.0-beta.3",
         "@types/rison": "^0.0.9",
         "tailwindcss": "^3.4.0",
         "postcss": "^8"
@@ -29,7 +30,9 @@
         "build:v2": "next build",
         "start": "next start",
         "build:v2:cloudflare": "opennextjs-cloudflare build",
-        "dev:v2:cloudflare": "wrangler dev --port 8771",
+        "dev:v2:cloudflare": "wrangler dev --port 8771 --env preview",
+        "dev:v2:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
+        "dev:v2:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
         "unit": "bun test",
         "typecheck": "tsc --noEmit"
     }
diff --git a/packages/gitbook-v2/src/app/global-error.tsx b/packages/gitbook-v2/src/app/global-error.tsx
new file mode 100644
index 0000000000..abf0186a40
--- /dev/null
+++ b/packages/gitbook-v2/src/app/global-error.tsx
@@ -0,0 +1,18 @@
+'use client';
+
+import NextError from 'next/error';
+
+export default function GlobalError({
+    error,
+}: {
+    error: Error & { digest?: string };
+}) {
+    console.error('Global error:', error);
+    return (
+        <html lang="en">
+            <body>
+                <NextError statusCode={undefined as any} />
+            </body>
+        </html>
+    );
+}
diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms-full.txt/route.ts b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms-full.txt/route.ts
new file mode 100644
index 0000000000..7702d6101c
--- /dev/null
+++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms-full.txt/route.ts
@@ -0,0 +1,14 @@
+import type { NextRequest } from 'next/server';
+
+import { serveLLMsFullTxt } from '@/routes/llms-full';
+import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils';
+
+export const dynamic = 'force-static';
+
+export async function GET(
+    _request: NextRequest,
+    { params }: { params: Promise<RouteLayoutParams> }
+) {
+    const { context } = await getStaticSiteContext(await params);
+    return serveLLMsFullTxt(context);
+}
diff --git a/packages/gitbook-v2/src/app/utils.ts b/packages/gitbook-v2/src/app/utils.ts
index da810b3b90..932902df0e 100644
--- a/packages/gitbook-v2/src/app/utils.ts
+++ b/packages/gitbook-v2/src/app/utils.ts
@@ -1,4 +1,5 @@
 import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
+import { getDynamicCustomizationSettings } from '@/lib/customization';
 import type { SiteAPIToken } from '@gitbook/api';
 import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
 import { jwtDecode } from 'jwt-decode';
@@ -67,6 +68,8 @@ export async function getDynamicSiteContext(params: RouteLayoutParams) {
         siteURLData
     );
 
+    context.customization = await getDynamicCustomizationSettings(context.customization);
+
     return {
         context,
         visitorAuthClaims: getVisitorAuthClaims(siteURLData),
diff --git a/packages/gitbook-v2/src/lib/cache.test.ts b/packages/gitbook-v2/src/lib/cache.test.ts
new file mode 100644
index 0000000000..735b1fb06d
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/cache.test.ts
@@ -0,0 +1,80 @@
+import { describe, expect, it } from 'bun:test';
+import { withStableRef } from './cache';
+
+describe('withStableRef', () => {
+    it('should return primitive values as is', () => {
+        const toStableRef = withStableRef();
+
+        expect(toStableRef(42)).toBe(42);
+        expect(toStableRef('hello')).toBe('hello');
+        expect(toStableRef(true)).toBe(true);
+        expect(toStableRef(null)).toBe(null);
+        expect(toStableRef(undefined)).toBe(undefined);
+    });
+
+    it('should return the same reference for identical objects', () => {
+        const toStableRef = withStableRef();
+
+        const obj1 = { a: 1, b: 2 };
+        const obj2 = { a: 1, b: 2 };
+
+        const ref1 = toStableRef(obj1);
+        const ref2 = toStableRef(obj2);
+
+        expect(ref1).toBe(ref2);
+        expect(ref1).toBe(obj1);
+        expect(ref1).not.toBe(obj2);
+    });
+
+    it('should return the same reference for identical arrays', () => {
+        const toStableRef = withStableRef();
+
+        const arr1 = [1, 2, 3];
+        const arr2 = [1, 2, 3];
+
+        const ref1 = toStableRef(arr1);
+        const ref2 = toStableRef(arr2);
+
+        expect(ref1).toBe(ref2);
+        expect(ref1).toBe(arr1);
+        expect(ref1).not.toBe(arr2);
+    });
+
+    it('should return the same reference for identical nested objects', () => {
+        const toStableRef = withStableRef();
+
+        const obj1 = { a: { b: 1 }, c: [2, 3] };
+        const obj2 = { a: { b: 1 }, c: [2, 3] };
+
+        const ref1 = toStableRef(obj1);
+        const ref2 = toStableRef(obj2);
+
+        expect(ref1).toBe(ref2);
+        expect(ref1).toBe(obj1);
+        expect(ref1).not.toBe(obj2);
+    });
+
+    it('should return different references for different objects', () => {
+        const toStableRef = withStableRef();
+
+        const obj1 = { a: 1 };
+        const obj2 = { a: 2 };
+
+        const ref1 = toStableRef(obj1);
+        const ref2 = toStableRef(obj2);
+
+        expect(ref1).not.toBe(ref2);
+    });
+
+    it('should maintain reference stability across multiple calls', () => {
+        const toStableRef = withStableRef();
+
+        const obj = { a: 1 };
+        const ref1 = toStableRef(obj);
+        const ref2 = toStableRef(obj);
+        const ref3 = toStableRef(obj);
+
+        expect(ref1).toBe(ref2);
+        expect(ref2).toBe(ref3);
+    });
+});
diff --git a/packages/gitbook-v2/src/lib/cache.ts b/packages/gitbook-v2/src/lib/cache.ts
new file mode 100644
index 0000000000..e05753907a
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/cache.ts
@@ -0,0 +1,59 @@
+import { identify } from 'object-identity';
+import * as React from 'react';
+
+/**
+ * Equivalent to `React.cache` but with support for non-primitive arguments.
+ * As `React.cache` only uses `Object.is` to compare arguments, it will not work with non-primitive arguments.
+ */
+export function cache<Args extends any[], Return>(fn: (...args: Args) => Return) {
+    const cached = React.cache(fn);
+
+    return (...args: Args) => {
+        const toStableRef = getWithStableRef();
+        const stableArgs = args.map((value) => {
+            return toStableRef(value);
+        }) as Args;
+        return cached(...stableArgs);
+    };
+}
+
+/**
+ * To ensure memory is garbage collected between each request, we use a per-request cache to store the ref maps.
+ */
+const getWithStableRef = React.cache(withStableRef);
+
+/**
+ * Create a function that converts a value to a stable reference.
+ */
+export function withStableRef(): <T>(value: T) => T {
+    const reverseIndex = new WeakMap<object, string>();
+    const refIndex = new Map<string, object>();
+
+    return <T>(value: T) => {
+        if (isPrimitive(value)) {
+            return value;
+        }
+
+        const objectValue = value as object;
+        const index = reverseIndex.get(objectValue);
+        if (index !== undefined) {
+            return refIndex.get(index) as T;
+        }
+
+        const hash = identify(objectValue);
+        reverseIndex.set(objectValue, hash);
+
+        const existing = refIndex.get(hash);
+        if (existing !== undefined) {
+            return existing as T;
+        }
+
+        // first time we've seen this shape
+        refIndex.set(hash, objectValue);
+        return value;
+    };
+}
+
+function isPrimitive(value: any): boolean {
+    return value === null || typeof value !== 'object';
+}
diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts
index d25dd1946e..ba0f299574 100644
--- a/packages/gitbook-v2/src/lib/context.ts
+++ b/packages/gitbook-v2/src/lib/context.ts
@@ -19,6 +19,7 @@ import {
     getDataOrNull,
     throwIfDataError,
 } from '@v2/lib/data';
+import assertNever from 'assert-never';
 import { notFound } from 'next/navigation';
 import { assert } from 'ts-essentials';
 import { GITBOOK_URL } from './env';
@@ -164,7 +165,7 @@ export function getBaseContext(input: {
         // Create link in the same format for links to other sites/sections.
         linker.toLinkForContent = (rawURL: string) => {
             const urlObject = new URL(rawURL);
-            return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}`;
+            return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}${urlObject.hash}`;
         };
     }
 
@@ -242,19 +243,45 @@ export async function fetchSiteContextByIds(
         ? parseSiteSectionsAndGroups(siteStructure, ids.siteSection)
         : null;
 
-    const siteSpace = (
-        siteStructure.type === 'siteSpaces' && siteStructure.structure
-            ? siteStructure.structure
-            : sections?.current.siteSpaces
-    )?.find((siteSpace) => siteSpace.id === ids.siteSpace);
-    if (!siteSpace) {
-        throw new Error('Site space not found');
-    }
+    // Parse the current siteSpace and siteSpaces based on the site structure type.
+    const { siteSpaces, siteSpace }: { siteSpaces: SiteSpace[]; siteSpace: SiteSpace } = (() => {
+        if (siteStructure.type === 'siteSpaces') {
+            const siteSpaces = siteStructure.structure;
+            const siteSpace = siteSpaces.find((siteSpace) => siteSpace.id === ids.siteSpace);
+
+            if (!siteSpace) {
+                throw new Error(
+                    `Site space "${ids.siteSpace}" not found in structure type="siteSpaces"`
+                );
+            }
+
+            return { siteSpaces, siteSpace };
+        }
+
+        if (siteStructure.type === 'sections') {
+            assert(
+                sections,
+                `cannot find site space "${ids.siteSpace}" because parsed sections are missing siteStructure.type="sections" siteSection="${ids.siteSection}"`
+            );
 
-    const siteSpaces =
-        siteStructure.type === 'siteSpaces'
-            ? siteStructure.structure
-            : (sections?.current.siteSpaces ?? []);
+            const currentSection = sections.current;
+            const siteSpaces = currentSection.siteSpaces;
+            const siteSpace = currentSection.siteSpaces.find(
+                (siteSpace) => siteSpace.id === ids.siteSpace
+            );
+
+            if (!siteSpace) {
+                throw new Error(
+                    `Site space "${ids.siteSpace}" not found in structure type="sections" currentSection="${currentSection.id}"`
+                );
+            }
+
+            return { siteSpaces, siteSpace };
+        }
+
+        // @ts-expect-error
+        assertNever(siteStructure, `cannot handle site structure of type ${siteStructure.type}`);
+    })();
 
     const customization = (() => {
         if (ids.siteSpace) {
@@ -380,7 +407,7 @@ export function checkIsRootSiteContext(context: GitBookSiteContext): boolean {
 function parseSiteSectionsAndGroups(structure: SiteStructure, siteSectionId: string) {
     const sectionsAndGroups = getSiteStructureSections(structure, { ignoreGroups: false });
     const section = parseCurrentSection(structure, siteSectionId);
-    assert(section, 'A section must be defined when there are multiple sections');
+    assert(section, `couldn't find section "${siteSectionId}" in site structure`);
     return { list: sectionsAndGroups, current: section } satisfies SiteSections;
 }
 
diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts
index 20e495656e..8e6535331a 100644
--- a/packages/gitbook-v2/src/lib/data/api.ts
+++ b/packages/gitbook-v2/src/lib/data/api.ts
@@ -2,22 +2,14 @@ import { trace } from '@/lib/tracing';
 import {
     type ComputedContentSource,
     GitBookAPI,
-    type GitBookAPIServiceBinding,
     type HttpResponse,
     type RenderIntegrationUI,
 } from '@gitbook/api';
 import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
-import {
-    GITBOOK_API_TOKEN,
-    GITBOOK_API_URL,
-    GITBOOK_RUNTIME,
-    GITBOOK_USER_AGENT,
-} from '@v2/lib/env';
+import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
 import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
-import { unstable_cache } from 'next/cache';
-import { getCloudflareContext, getCloudflareRequestGlobal } from './cloudflare';
-import { DataFetcherError, wrapDataFetcherError } from './errors';
-import { withCacheKey, withoutConcurrentExecution } from './memoize';
+import { cache } from '../cache';
+import { DataFetcherError, wrapCacheDataFetcherError } from './errors';
 import type { GitBookDataFetcher } from './types';
 
 interface DataFetcherInput {
@@ -28,16 +20,13 @@ interface DataFetcherInput {
 }
 
 /**
- * Revalidation profile for the cache.
- * Based on https://nextjs.org/docs/app/api-reference/functions/cacheLife#default-cache-profiles
+ * Options to pass to the `fetch` call to disable the Next data-cache when wrapped in `use cache`.
  */
-enum RevalidationProfile {
-    minutes = 60,
-    hours = 60 * 60,
-    days = 60 * 60 * 24,
-    weeks = 60 * 60 * 24 * 7,
-    max = 60 * 60 * 24 * 30,
-}
+export const noCacheFetchOptions: Partial<RequestInit> = {
+    next: {
+        revalidate: 0,
+    },
+};
 
 /**
  * Create a data fetcher using an API token.
@@ -124,6 +113,15 @@ export function createDataFetcher(
                 })
             );
         },
+        getRevisionPageDocument(params) {
+            return trace('getRevisionPageDocument', () =>
+                getRevisionPageDocument(input, {
+                    spaceId: params.spaceId,
+                    revisionId: params.revisionId,
+                    pageId: params.pageId,
+                })
+            );
+        },
         getReusableContent(params) {
             return trace('getReusableContent', () =>
                 getReusableContent(input, {
@@ -206,186 +204,54 @@ export function createDataFetcher(
     };
 }
 
-/*
- * For the following functions, we:
- * - Wrap them with `withCacheKey` to compute a cache key from the function arguments ONCE (to be performant)
- * - Pass the cache key to `unstable_cache` to ensure the cache is not tied to closures
- * - Call the uncached function in a `withoutConcurrentExecution` wrapper to prevent concurrent executions
- *
- * Important:
- * - Only the function inside the `unstable_cache` is wrapped in `withoutConcurrentExecution` as Next.js needs to call
- *   the return of `unstable_cache` to identify the tags.
- */
-
-const getUserById = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (cacheKey, input: DataFetcherInput, params: { userId: string }) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getUserByIdUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getUserByIdUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.days,
-                    tags: [],
-                }
-            );
-
-            return uncached();
-        }
-    )
-);
-
-const getUserByIdUseCache = async (input: DataFetcherInput, params: { userId: string }) => {
+const getUserById = cache(async (input: DataFetcherInput, params: { userId: string }) => {
     'use cache';
-    return getUserByIdUncached(input, params, true);
-};
-
-const getUserByIdUncached = async (
-    input: DataFetcherInput,
-    params: { userId: string },
-    withUseCache = false
-) => {
-    return trace(`getUserById.uncached(${params.userId})`, async () => {
-        return wrapDataFetcherError(async () => {
+    return wrapCacheDataFetcherError(async () => {
+        return trace(`getUserById(${params.userId})`, async () => {
             const api = apiClient(input);
-            const res = await api.users.getUserById(params.userId);
-            if (withUseCache) {
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('days');
-            }
+            const res = await api.users.getUserById(params.userId, {
+                ...noCacheFetchOptions,
+            });
+            cacheTag(...getCacheTagsFromResponse(res));
+            cacheLife('days');
             return res.data;
         });
     });
-};
-
-const getSpace = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { spaceId: string; shareKey: string | undefined }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getSpaceUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getSpaceUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.days,
-                    tags: [
-                        getCacheTag({
-                            tag: 'space',
-                            space: params.spaceId,
-                        }),
-                    ],
-                }
-            );
-
-            return uncached();
-        }
-    )
-);
+});
 
-const getSpaceUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; shareKey: string | undefined }
-) => {
-    'use cache';
-    return getSpaceUncached(input, params, true);
-};
-
-const getSpaceUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; shareKey: string | undefined },
-    withUseCache = false
-) => {
-    if (withUseCache) {
+const getSpace = cache(
+    async (input: DataFetcherInput, params: { spaceId: string; shareKey: string | undefined }) => {
+        'use cache';
         cacheTag(
             getCacheTag({
                 tag: 'space',
                 space: params.spaceId,
             })
         );
-    }
 
-    return trace(`getSpace.uncached(${params.spaceId}, ${params.shareKey})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getSpaceById(params.spaceId, {
-                shareKey: params.shareKey,
-            });
-
-            if (withUseCache) {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getSpaceById(
+                    params.spaceId,
+                    {
+                        shareKey: params.shareKey,
+                    },
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
                 cacheTag(...getCacheTagsFromResponse(res));
                 cacheLife('days');
-            }
-            return res.data;
+                return res.data;
+            });
         });
-    });
-};
-
-const getChangeRequest = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { spaceId: string; changeRequestId: string }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getChangeRequestUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getChangeRequestUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.minutes * 5,
-                    tags: [
-                        getCacheTag({
-                            tag: 'change-request',
-                            space: params.spaceId,
-                            changeRequest: params.changeRequestId,
-                        }),
-                    ],
-                }
-            );
-
-            return uncached();
-        }
-    )
+    }
 );
 
-const getChangeRequestUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; changeRequestId: string }
-) => {
-    'use cache';
-    return getChangeRequestUncached(input, params, true);
-};
-
-const getChangeRequestUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; changeRequestId: string },
-    withUseCache = false
-) => {
-    if (withUseCache) {
+const getChangeRequest = cache(
+    async (input: DataFetcherInput, params: { spaceId: string; changeRequestId: string }) => {
+        'use cache';
         cacheTag(
             getCacheTag({
                 tag: 'change-request',
@@ -393,461 +259,243 @@ const getChangeRequestUncached = async (
                 changeRequest: params.changeRequestId,
             })
         );
-    }
 
-    return trace(
-        `getChangeRequest.uncached(${params.spaceId}, ${params.changeRequestId})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getChangeRequestById(
-                    params.spaceId,
-                    params.changeRequestId
-                );
-                if (withUseCache) {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`,
+                async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getChangeRequestById(
+                        params.spaceId,
+                        params.changeRequestId,
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('minutes');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
-const getRevision = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; metadata: boolean }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getRevisionUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getRevisionUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
-                }
-            );
-
-            return uncached();
-        }
-    )
-);
-
-const getRevisionUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; metadata: boolean }
-) => {
-    'use cache';
-    return getRevisionUncached(input, params, true);
-};
-
-const getRevisionUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; metadata: boolean },
-    withUseCache = false
-) => {
-    return trace(`getRevision.uncached(${params.spaceId}, ${params.revisionId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getRevisionById(params.spaceId, params.revisionId, {
-                metadata: params.metadata,
-            });
-            if (withUseCache) {
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
-            }
-            return res.data;
-        });
-    });
-};
-
-const getRevisionPages = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; metadata: boolean }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getRevisionPagesUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getRevisionPagesUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
-                }
-            );
-
-            return uncached();
-        }
-    )
-);
-
-const getRevisionPagesUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; metadata: boolean }
-) => {
-    'use cache';
-    return getRevisionPagesUncached(input, params, true);
-};
-
-const getRevisionPagesUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; metadata: boolean },
-    withUseCache = false
-) => {
-    return trace(`getRevisionPages.uncached(${params.spaceId}, ${params.revisionId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.listPagesInRevisionById(
-                params.spaceId,
-                params.revisionId,
-                {
-                    metadata: params.metadata,
+                    return res.data;
                 }
             );
-            if (withUseCache) {
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
-            }
-            return res.data.pages;
         });
-    });
-};
-
-const getRevisionFile = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; fileId: string }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getRevisionFileUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getRevisionFileUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
-                }
-            );
-
-            return uncached();
-        }
-    )
+    }
 );
 
-const getRevisionFileUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; fileId: string }
-) => {
-    'use cache';
-    return getRevisionFileUncached(input, params, true);
-};
-
-const getRevisionFileUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; fileId: string },
-    withUseCache = false
-) => {
-    return trace(
-        `getRevisionFile.uncached(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
+const getRevision = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; metadata: boolean }
+    ) => {
+        'use cache';
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
                 const api = apiClient(input);
-                const res = await api.spaces.getFileInRevisionById(
+                const res = await api.spaces.getRevisionById(
                     params.spaceId,
                     params.revisionId,
-                    params.fileId,
-                    {}
+                    {
+                        metadata: params.metadata,
+                    },
+                    {
+                        ...noCacheFetchOptions,
+                    }
                 );
-                if (withUseCache) {
-                    cacheTag(...getCacheTagsFromResponse(res));
-                    cacheLife('max');
-                }
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
                 return res.data;
             });
-        }
-    );
-};
-
-const getRevisionPageMarkdown = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; pageId: string }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getRevisionPageMarkdownUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getRevisionPageMarkdownUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
-                }
-            );
-
-            return uncached();
-        }
-    )
+        });
+    }
 );
 
-const getRevisionPageMarkdownUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; pageId: string }
-) => {
-    'use cache';
-    return getRevisionPageMarkdownUncached(input, params, true);
-};
-
-const getRevisionPageMarkdownUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; pageId: string },
-    withUseCache = false
-) => {
-    return trace(
-        `getRevisionPageMarkdown.uncached(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
+const getRevisionPages = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; metadata: boolean }
+    ) => {
+        'use cache';
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => {
                 const api = apiClient(input);
-                const res = await api.spaces.getPageInRevisionById(
+                const res = await api.spaces.listPagesInRevisionById(
                     params.spaceId,
                     params.revisionId,
-                    params.pageId,
                     {
-                        format: 'markdown',
+                        metadata: params.metadata,
+                    },
+                    {
+                        ...noCacheFetchOptions,
                     }
                 );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
+                return res.data.pages;
+            });
+        });
+    }
+);
 
-                if (withUseCache) {
+const getRevisionFile = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; fileId: string }
+    ) => {
+        'use cache';
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`,
+                async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getFileInRevisionById(
+                        params.spaceId,
+                        params.revisionId,
+                        params.fileId,
+                        {},
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
+                    return res.data;
                 }
+            );
+        });
+    }
+);
 
-                if (!('markdown' in res.data)) {
-                    throw new DataFetcherError('Page is not a document', 404);
-                }
-                return res.data.markdown;
-            });
-        }
-    );
-};
+const getRevisionPageMarkdown = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; pageId: string }
+    ) => {
+        'use cache';
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
+                async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getPageInRevisionById(
+                        params.spaceId,
+                        params.revisionId,
+                        params.pageId,
+                        {
+                            format: 'markdown',
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
 
-const getRevisionPageByPath = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; path: string }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getRevisionPageByPathUseCache(input, params);
-            }
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('max');
 
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getRevisionPageByPathUncached(input, params);
-                },
-                [cacheKey, 'v2'],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
+                    if (!('markdown' in res.data)) {
+                        throw new DataFetcherError('Page is not a document', 404);
+                    }
+                    return res.data.markdown;
                 }
             );
-
-            return uncached();
-        }
-    )
+        });
+    }
 );
 
-const getRevisionPageByPathUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; path: string }
-) => {
-    'use cache';
-    return getRevisionPageByPathUncached(input, params, true);
-};
+const getRevisionPageDocument = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; pageId: string }
+    ) => {
+        'use cache';
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
+                async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getPageDocumentInRevisionById(
+                        params.spaceId,
+                        params.revisionId,
+                        params.pageId,
+                        {
+                            evaluated: true,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
 
-const getRevisionPageByPathUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; path: string },
-    withUseCache = false
-) => {
-    return trace(
-        `getRevisionPageByPath.uncached(${params.spaceId}, ${params.revisionId}, ${params.path})`,
-        async () => {
-            const encodedPath = encodeURIComponent(params.path);
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getPageInRevisionByPath(
-                    params.spaceId,
-                    params.revisionId,
-                    encodedPath,
-                    {}
-                );
-                if (withUseCache) {
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
-const getDocument = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { spaceId: string; documentId: string }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getDocumentUseCache(input, params);
-            }
 
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getDocumentUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
+                    return res.data;
                 }
             );
-
-            return uncached();
-        }
-    )
-);
-
-const getDocumentUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; documentId: string }
-) => {
-    'use cache';
-    return getDocumentUncached(input, params, true);
-};
-
-const getDocumentUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; documentId: string },
-    withUseCache = false
-) => {
-    return trace(`getDocument.uncached(${params.spaceId}, ${params.documentId})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getDocumentById(params.spaceId, params.documentId, {});
-            if (withUseCache) {
-                cacheTag(...getCacheTagsFromResponse(res));
-                cacheLife('max');
-            }
-            return res.data;
         });
-    });
-};
-
-const getComputedDocument = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: {
-                spaceId: string;
-                organizationId: string;
-                source: ComputedContentSource;
-                seed: string;
-            }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getComputedDocumentUseCache(input, params);
-            }
+    }
+);
 
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
+const getRevisionPageByPath = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; path: string }
+    ) => {
+        'use cache';
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
                 async () => {
-                    return getComputedDocumentUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: getComputedContentSourceCacheTags(
+                    const encodedPath = encodeURIComponent(params.path);
+                    const api = apiClient(input);
+                    const res = await api.spaces.getPageInRevisionByPath(
+                        params.spaceId,
+                        params.revisionId,
+                        encodedPath,
+                        {},
                         {
-                            spaceId: params.spaceId,
-                            organizationId: params.organizationId,
-                        },
-                        params.source
-                    ),
+                            ...noCacheFetchOptions,
+                        }
+                    );
+                    cacheTag(...getCacheTagsFromResponse(res));
+                    cacheLife('max');
+                    return res.data;
                 }
             );
-
-            return uncached();
-        }
-    )
+        });
+    }
 );
 
-const getComputedDocumentUseCache = async (
-    input: DataFetcherInput,
-    params: {
-        spaceId: string;
-        organizationId: string;
-        source: ComputedContentSource;
-        seed: string;
+const getDocument = cache(
+    async (input: DataFetcherInput, params: { spaceId: string; documentId: string }) => {
+        'use cache';
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getDocumentById(
+                    params.spaceId,
+                    params.documentId,
+                    {},
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
+                cacheTag(...getCacheTagsFromResponse(res));
+                cacheLife('max');
+                return res.data;
+            });
+        });
     }
-) => {
-    'use cache';
-    return getComputedDocumentUncached(input, params, true);
-};
+);
 
-const getComputedDocumentUncached = async (
-    input: DataFetcherInput,
-    params: {
-        spaceId: string;
-        organizationId: string;
-        source: ComputedContentSource;
-        seed: string;
-    },
-    withUseCache = false
-) => {
-    if (withUseCache) {
+const getComputedDocument = cache(
+    async (
+        input: DataFetcherInput,
+        params: {
+            spaceId: string;
+            organizationId: string;
+            source: ComputedContentSource;
+            seed: string;
+        }
+    ) => {
+        'use cache';
         cacheTag(
             ...getComputedContentSourceCacheTags(
                 {
@@ -857,138 +505,64 @@ const getComputedDocumentUncached = async (
                 params.source
             )
         );
-    }
 
-    return trace(
-        `getComputedDocument.uncached(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getComputedDocument(params.spaceId, {
-                    source: params.source,
-                    seed: params.seed,
-                });
-                if (withUseCache) {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`,
+                async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getComputedDocument(
+                        params.spaceId,
+                        {
+                            source: params.source,
+                            seed: params.seed,
+                        },
+                        {},
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
-const getReusableContent = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { spaceId: string; revisionId: string; reusableContentId: string }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getReusableContentUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getReusableContentUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [],
+                    return res.data;
                 }
             );
-
-            return uncached();
-        }
-    )
+        });
+    }
 );
 
-const getReusableContentUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; reusableContentId: string }
-) => {
-    'use cache';
-    return getReusableContentUncached(input, params, true);
-};
-
-const getReusableContentUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; revisionId: string; reusableContentId: string },
-    withUseCache = false
-) => {
-    return trace(
-        `getReusableContent.uncached(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.spaces.getReusableContentInRevisionById(
-                    params.spaceId,
-                    params.revisionId,
-                    params.reusableContentId
-                );
-                if (withUseCache) {
+const getReusableContent = cache(
+    async (
+        input: DataFetcherInput,
+        params: { spaceId: string; revisionId: string; reusableContentId: string }
+    ) => {
+        'use cache';
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
+                async () => {
+                    const api = apiClient(input);
+                    const res = await api.spaces.getReusableContentInRevisionById(
+                        params.spaceId,
+                        params.revisionId,
+                        params.reusableContentId,
+                        {},
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
-const getLatestOpenAPISpecVersionContent = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { organizationId: string; slug: string }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getLatestOpenAPISpecVersionContentUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getLatestOpenAPISpecVersionContentUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.max,
-                    tags: [
-                        getCacheTag({
-                            tag: 'openapi',
-                            organization: params.organizationId,
-                            openAPISpec: params.slug,
-                        }),
-                    ],
+                    return res.data;
                 }
             );
-
-            return uncached();
-        }
-    )
+        });
+    }
 );
 
-const getLatestOpenAPISpecVersionContentUseCache = async (
-    input: DataFetcherInput,
-    params: { organizationId: string; slug: string }
-) => {
-    'use cache';
-    return getLatestOpenAPISpecVersionContentUncached(input, params, true);
-};
-
-const getLatestOpenAPISpecVersionContentUncached = async (
-    input: DataFetcherInput,
-    params: { organizationId: string; slug: string },
-    withUseCache = false
-) => {
-    if (withUseCache) {
+const getLatestOpenAPISpecVersionContent = cache(
+    async (input: DataFetcherInput, params: { organizationId: string; slug: string }) => {
+        'use cache';
         cacheTag(
             getCacheTag({
                 tag: 'openapi',
@@ -996,441 +570,242 @@ const getLatestOpenAPISpecVersionContentUncached = async (
                 openAPISpec: params.slug,
             })
         );
-    }
 
-    return trace(
-        `getLatestOpenAPISpecVersionContent.uncached(${params.organizationId}, ${params.slug})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.orgs.getLatestOpenApiSpecVersionContent(
-                    params.organizationId,
-                    params.slug
-                );
-                if (withUseCache) {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`,
+                async () => {
+                    const api = apiClient(input);
+                    const res = await api.orgs.getLatestOpenApiSpecVersionContent(
+                        params.organizationId,
+                        params.slug,
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('max');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
-const getPublishedContentSite = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { organizationId: string; siteId: string; siteShareKey: string | undefined }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getPublishedContentSiteUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getPublishedContentSiteUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.days,
-                    tags: [
-                        getCacheTag({
-                            tag: 'site',
-                            site: params.siteId,
-                        }),
-                    ],
+                    return res.data;
                 }
             );
-
-            return uncached();
-        }
-    )
+        });
+    }
 );
 
-const getPublishedContentSiteUseCache = async (
-    input: DataFetcherInput,
-    params: { organizationId: string; siteId: string; siteShareKey: string | undefined }
-) => {
-    'use cache';
-    return getPublishedContentSiteUncached(input, params, true);
-};
-
-const getPublishedContentSiteUncached = async (
-    input: DataFetcherInput,
-    params: { organizationId: string; siteId: string; siteShareKey: string | undefined },
-    withUseCache = false
-) => {
-    if (withUseCache) {
+const getPublishedContentSite = cache(
+    async (
+        input: DataFetcherInput,
+        params: { organizationId: string; siteId: string; siteShareKey: string | undefined }
+    ) => {
+        'use cache';
         cacheTag(
             getCacheTag({
                 tag: 'site',
                 site: params.siteId,
             })
         );
-    }
 
-    return trace(
-        `getPublishedContentSite.uncached(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.orgs.getPublishedContentSite(
-                    params.organizationId,
-                    params.siteId,
-                    {
-                        shareKey: params.siteShareKey,
-                    }
-                );
-                if (withUseCache) {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`,
+                async () => {
+                    const api = apiClient(input);
+                    const res = await api.orgs.getPublishedContentSite(
+                        params.organizationId,
+                        params.siteId,
+                        {
+                            shareKey: params.siteShareKey,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('days');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
-const getSiteRedirectBySource = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: {
-                organizationId: string;
-                siteId: string;
-                siteShareKey: string | undefined;
-                source: string;
-            }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getSiteRedirectBySourceUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getSiteRedirectBySourceUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.days,
-                    tags: [
-                        getCacheTag({
-                            tag: 'site',
-                            site: params.siteId,
-                        }),
-                    ],
+                    return res.data;
                 }
             );
-
-            return uncached();
-        }
-    )
-);
-
-const getSiteRedirectBySourceUseCache = async (
-    input: DataFetcherInput,
-    params: {
-        organizationId: string;
-        siteId: string;
-        siteShareKey: string | undefined;
-        source: string;
+        });
     }
-) => {
-    'use cache';
-    return getSiteRedirectBySourceUncached(input, params, true);
-};
+);
 
-const getSiteRedirectBySourceUncached = async (
-    input: DataFetcherInput,
-    params: {
-        organizationId: string;
-        siteId: string;
-        siteShareKey: string | undefined;
-        source: string;
-    },
-    withUseCache = false
-) => {
-    if (withUseCache) {
+const getSiteRedirectBySource = cache(
+    async (
+        input: DataFetcherInput,
+        params: {
+            organizationId: string;
+            siteId: string;
+            siteShareKey: string | undefined;
+            source: string;
+        }
+    ) => {
+        'use cache';
         cacheTag(
             getCacheTag({
                 tag: 'site',
                 site: params.siteId,
             })
         );
-    }
 
-    return trace(
-        `getSiteRedirectBySource.uncached(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const api = apiClient(input);
-                const res = await api.orgs.getSiteRedirectBySource(
-                    params.organizationId,
-                    params.siteId,
-                    {
-                        shareKey: params.siteShareKey,
-                        source: params.source,
-                    }
-                );
-                if (withUseCache) {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`,
+                async () => {
+                    const api = apiClient(input);
+                    const res = await api.orgs.getSiteRedirectBySource(
+                        params.organizationId,
+                        params.siteId,
+                        {
+                            shareKey: params.siteShareKey,
+                            source: params.source,
+                        },
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('days');
-                }
-                return res.data;
-            });
-        }
-    );
-};
-
-const getEmbedByUrl = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (cacheKey, input: DataFetcherInput, params: { spaceId: string; url: string }) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return getEmbedByUrlUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return getEmbedByUrlUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.weeks,
-                    tags: [],
+                    return res.data;
                 }
             );
-
-            return uncached();
-        }
-    )
+        });
+    }
 );
 
-const getEmbedByUrlUseCache = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; url: string }
-) => {
-    'use cache';
-    return getEmbedByUrlUncached(input, params, true);
-};
-
-const getEmbedByUrlUncached = async (
-    input: DataFetcherInput,
-    params: { spaceId: string; url: string },
-    withUseCache = false
-) => {
-    if (withUseCache) {
+const getEmbedByUrl = cache(
+    async (input: DataFetcherInput, params: { spaceId: string; url: string }) => {
+        'use cache';
         cacheTag(
             getCacheTag({
                 tag: 'space',
                 space: params.spaceId,
             })
         );
-    }
 
-    return trace(`getEmbedByUrl.uncached(${params.spaceId}, ${params.url})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, {
-                url: params.url,
-            });
-            if (withUseCache) {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
+                const api = apiClient(input);
+                const res = await api.spaces.getEmbedByUrlInSpace(
+                    params.spaceId,
+                    {
+                        url: params.url,
+                    },
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
                 cacheTag(...getCacheTagsFromResponse(res));
                 cacheLife('weeks');
-            }
-            return res.data;
+                return res.data;
+            });
         });
-    });
-};
-
-const searchSiteContent = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return searchSiteContentUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return searchSiteContentUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.hours,
-                    tags: [],
-                }
-            );
-
-            return uncached();
-        }
-    )
+    }
 );
 
-const searchSiteContentUseCache = async (
-    input: DataFetcherInput,
-    params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
-) => {
-    'use cache';
-    return searchSiteContentUncached(input, params, true);
-};
-
-const searchSiteContentUncached = async (
-    input: DataFetcherInput,
-    params: Parameters<GitBookDataFetcher['searchSiteContent']>[0],
-    withUseCache = false
-) => {
-    if (withUseCache) {
+const searchSiteContent = cache(
+    async (
+        input: DataFetcherInput,
+        params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
+    ) => {
+        'use cache';
         cacheTag(
             getCacheTag({
                 tag: 'site',
                 site: params.siteId,
             })
         );
-    }
 
-    return trace(
-        `searchSiteContent.uncached(${params.organizationId}, ${params.siteId}, ${params.query})`,
-        async () => {
-            return wrapDataFetcherError(async () => {
-                const { organizationId, siteId, query, scope } = params;
-                const api = apiClient(input);
-                const res = await api.orgs.searchSiteContent(organizationId, siteId, {
-                    query,
-                    ...scope,
-                });
-                if (withUseCache) {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(
+                `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`,
+                async () => {
+                    const { organizationId, siteId, query, scope } = params;
+                    const api = apiClient(input);
+                    const res = await api.orgs.searchSiteContent(
+                        organizationId,
+                        siteId,
+                        {
+                            query,
+                            ...scope,
+                        },
+                        {},
+                        {
+                            ...noCacheFetchOptions,
+                        }
+                    );
                     cacheTag(...getCacheTagsFromResponse(res));
                     cacheLife('hours');
-                }
-                return res.data.items;
-            });
-        }
-    );
-};
-
-const renderIntegrationUi = withCacheKey(
-    withoutConcurrentExecution(
-        getCloudflareRequestGlobal,
-        async (
-            cacheKey,
-            input: DataFetcherInput,
-            params: { integrationName: string; request: RenderIntegrationUI }
-        ) => {
-            if (GITBOOK_RUNTIME !== 'cloudflare') {
-                return renderIntegrationUiUseCache(input, params);
-            }
-
-            // FIXME: OpenNext doesn't support 'use cache' yet
-            const uncached = unstable_cache(
-                async () => {
-                    return renderIntegrationUiUncached(input, params);
-                },
-                [cacheKey],
-                {
-                    revalidate: RevalidationProfile.days,
-                    tags: [
-                        getCacheTag({
-                            tag: 'integration',
-                            integration: params.integrationName,
-                        }),
-                    ],
+                    return res.data.items;
                 }
             );
-
-            return uncached();
-        }
-    )
+        });
+    }
 );
 
-const renderIntegrationUiUseCache = async (
-    input: DataFetcherInput,
-    params: { integrationName: string; request: RenderIntegrationUI }
-) => {
-    'use cache';
-    return renderIntegrationUiUncached(input, params, true);
-};
+const renderIntegrationUi = cache(
+    async (
+        input: DataFetcherInput,
+        params: { integrationName: string; request: RenderIntegrationUI }
+    ) => {
+        'use cache';
+        cacheTag(
+            getCacheTag({
+                tag: 'integration',
+                integration: params.integrationName,
+            })
+        );
 
-const renderIntegrationUiUncached = (
-    input: DataFetcherInput,
-    params: { integrationName: string; request: RenderIntegrationUI },
-    withUseCache = false
-) => {
-    return trace(`renderIntegrationUi.uncached(${params.integrationName})`, async () => {
-        return wrapDataFetcherError(async () => {
-            const api = apiClient(input);
-            const res = await api.integrations.renderIntegrationUiWithPost(
-                params.integrationName,
-                params.request
-            );
-            if (withUseCache) {
+        return wrapCacheDataFetcherError(async () => {
+            return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
+                const api = apiClient(input);
+                const res = await api.integrations.renderIntegrationUiWithPost(
+                    params.integrationName,
+                    params.request,
+                    {
+                        ...noCacheFetchOptions,
+                    }
+                );
                 cacheTag(...getCacheTagsFromResponse(res));
                 cacheLife('days');
-            }
-            return res.data;
+                return res.data;
+            });
         });
-    });
-};
+    }
+);
 
 async function* streamAIResponse(
     input: DataFetcherInput,
     params: Parameters<GitBookDataFetcher['streamAIResponse']>[0]
 ) {
     const api = apiClient(input);
-    const res = await api.orgs.streamAiResponseInSite(params.organizationId, params.siteId, {
-        input: params.input,
-        output: params.output,
-        model: params.model,
-    });
+    const res = await api.orgs.streamAiResponseInSite(
+        params.organizationId,
+        params.siteId,
+        {
+            input: params.input,
+            output: params.output,
+            model: params.model,
+        },
+        {
+            ...noCacheFetchOptions,
+        }
+    );
 
     for await (const event of res) {
         yield event;
     }
 }
 
-let loggedServiceBinding = false;
-
 /**
  * Create a new API client.
  */
 export function apiClient(input: DataFetcherInput = { apiToken: null }) {
     const { apiToken } = input;
-    let serviceBinding: GitBookAPIServiceBinding | undefined;
-
-    const cloudflareContext = getCloudflareContext();
-    if (cloudflareContext) {
-        // @ts-expect-error
-        serviceBinding = cloudflareContext.env.GITBOOK_API as GitBookAPIServiceBinding | undefined;
-        if (!loggedServiceBinding) {
-            loggedServiceBinding = true;
-            if (serviceBinding) {
-                // biome-ignore lint/suspicious/noConsole: we want to log here
-                console.log(`using service binding for the API (${GITBOOK_API_URL})`);
-            } else {
-                // biome-ignore lint/suspicious/noConsole: we want to log here
-                console.warn(`no service binding for the API (${GITBOOK_API_URL})`);
-            }
-        }
-    }
 
     const api = new GitBookAPI({
         authToken: apiToken || GITBOOK_API_TOKEN || undefined,
         endpoint: GITBOOK_API_URL,
         userAgent: GITBOOK_USER_AGENT,
-        serviceBinding,
     });
 
     return api;
diff --git a/packages/gitbook-v2/src/lib/data/cloudflare.ts b/packages/gitbook-v2/src/lib/data/cloudflare.ts
index ac3125603d..ca996690b6 100644
--- a/packages/gitbook-v2/src/lib/data/cloudflare.ts
+++ b/packages/gitbook-v2/src/lib/data/cloudflare.ts
@@ -11,15 +11,3 @@ export function getCloudflareContext() {
 
     return getCloudflareContextOpenNext();
 }
-
-/**
- * Return an object representing the current request.
- */
-export function getCloudflareRequestGlobal() {
-    const context = getCloudflareContext();
-    if (!context) {
-        return null;
-    }
-
-    return context.cf;
-}
diff --git a/packages/gitbook-v2/src/lib/data/errors.ts b/packages/gitbook-v2/src/lib/data/errors.ts
index 784eebdbb5..c4d983ec2a 100644
--- a/packages/gitbook-v2/src/lib/data/errors.ts
+++ b/packages/gitbook-v2/src/lib/data/errors.ts
@@ -1,4 +1,5 @@
 import { GitBookAPIError } from '@gitbook/api';
+import { unstable_cacheLife as cacheLife } from 'next/cache';
 import type { DataFetcherErrorData, DataFetcherResponse } from './types';
 
 export class DataFetcherError extends Error {
@@ -47,11 +48,7 @@ export function getDataOrNull<T>(
         return response.then((result) => getDataOrNull(result, ignoreErrors));
     }
 
-    if (response.error) {
-        if (ignoreErrors.includes(response.error.code)) return null;
-        throw new DataFetcherError(response.error.message, response.error.code);
-    }
-    return response.data;
+    return ignoreDataFetcherErrors(response, ignoreErrors).data ?? null;
 }
 
 /**
@@ -93,6 +90,50 @@ export async function wrapDataFetcherError<T>(
     }
 }
 
+/**
+ * Wrap an async execution to handle errors and return a DataFetcherResponse.
+ * This should be used inside 'use cache' functions.
+ */
+export async function wrapCacheDataFetcherError<T>(
+    fn: () => Promise<T>
+): Promise<DataFetcherResponse<T>> {
+    const result = await wrapDataFetcherError(fn);
+    if (result.error && result.error.code >= 500) {
+        // We don't want to cache errors for too long.
+        // as the API might
+        cacheLife('minutes');
+    }
+    return result;
+}
+
+/**
+ * Ignore some data fetcher errors.
+ */
+export function ignoreDataFetcherErrors<T>(
+    response: DataFetcherResponse<T>,
+    ignoreErrors?: number[]
+): DataFetcherResponse<T>;
+export function ignoreDataFetcherErrors<T>(
+    response: Promise<DataFetcherResponse<T>>,
+    ignoreErrors?: number[]
+): Promise<DataFetcherResponse<T>>;
+export function ignoreDataFetcherErrors<T>(
+    response: DataFetcherResponse<T> | Promise<DataFetcherResponse<T>>,
+    ignoreErrors: number[] = [404]
+): DataFetcherResponse<T> | Promise<DataFetcherResponse<T>> {
+    if (response instanceof Promise) {
+        return response.then((result) => ignoreDataFetcherErrors(result, ignoreErrors));
+    }
+
+    if (response.error) {
+        if (ignoreErrors.includes(response.error.code)) {
+            return response;
+        }
+        throw new DataFetcherError(response.error.message, response.error.code);
+    }
+    return response;
+}
+
 /**
  * Get a data fetcher exposable error from a JS error.
  */
diff --git a/packages/gitbook-v2/src/lib/data/index.ts b/packages/gitbook-v2/src/lib/data/index.ts
index d79049684a..93266d124e 100644
--- a/packages/gitbook-v2/src/lib/data/index.ts
+++ b/packages/gitbook-v2/src/lib/data/index.ts
@@ -1,8 +1,7 @@
 export * from './api';
 export * from './types';
-export * from './pages';
 export * from './urls';
 export * from './errors';
 export * from './lookup';
-export * from './proxy';
 export * from './visitor';
+export * from './pages';
diff --git a/packages/gitbook-v2/src/lib/data/lookup.ts b/packages/gitbook-v2/src/lib/data/lookup.ts
index 498f6e8859..be8c21f1d6 100644
--- a/packages/gitbook-v2/src/lib/data/lookup.ts
+++ b/packages/gitbook-v2/src/lib/data/lookup.ts
@@ -1,52 +1,46 @@
 import { race, tryCatch } from '@/lib/async';
 import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
 import { trace } from '@/lib/tracing';
-import type { PublishedSiteContentLookup } from '@gitbook/api';
+import type { PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api';
 import { apiClient } from './api';
 import { getExposableError } from './errors';
 import type { DataFetcherResponse } from './types';
 import { getURLLookupAlternatives, stripURLSearch } from './urls';
 
-/**
- * Lookup a content by its URL using the GitBook API.
- * To optimize caching, we try multiple lookup alternatives and return the first one that matches.
- */
-export async function getPublishedContentByURL(input: {
+interface LookupPublishedContentByUrlInput {
     url: string;
-    visitorAuthToken: string | null;
     redirectOnError: boolean;
     apiToken: string | null;
-}): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
+    visitorPayload: SiteVisitorPayload;
+}
+
+/**
+ * Lookup a content by its URL using the GitBook resolvePublishedContentByUrl API endpoint.
+ * To optimize caching, we try multiple lookup alternatives and return the first one that matches.
+ */
+export async function lookupPublishedContentByUrl(
+    input: LookupPublishedContentByUrlInput
+): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
     const lookupURL = new URL(input.url);
     const url = stripURLSearch(lookupURL);
     const lookup = getURLLookupAlternatives(url);
 
     const result = await race(lookup.urls, async (alternative, { signal }) => {
-        const api = await apiClient({ apiToken: input.apiToken });
-
+        const api = apiClient({ apiToken: input.apiToken });
         const callResult = await trace(
             {
-                operation: 'getPublishedContentByURL',
+                operation: 'resolvePublishedContentByUrl',
                 name: alternative.url,
             },
             () =>
                 tryCatch(
-                    api.urls.getPublishedContentByUrl(
+                    api.urls.resolvePublishedContentByUrl(
                         {
                             url: alternative.url,
-                            visitorAuthToken: input.visitorAuthToken ?? undefined,
+                            ...(input.visitorPayload ? { visitor: input.visitorPayload } : {}),
                             redirectOnError: input.redirectOnError,
-
-                            // As this endpoint is cached by our API, we version the request
-                            // to void getting stale data with missing properties.
-                            // this could be improved by ensuring our API cache layer is versioned
-                            // or invalidated when needed
-                            // @ts-expect-error - cacheVersion is not a real query param
-                            cacheVersion: 'v2',
                         },
-                        {
-                            signal,
-                        }
+                        { signal }
                     )
                 )
         );
diff --git a/packages/gitbook-v2/src/lib/data/memoize.test.ts b/packages/gitbook-v2/src/lib/data/memoize.test.ts
deleted file mode 100644
index 24c1014087..0000000000
--- a/packages/gitbook-v2/src/lib/data/memoize.test.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { describe, expect, it, mock } from 'bun:test';
-import { AsyncLocalStorage } from 'node:async_hooks';
-import { withCacheKey, withoutConcurrentExecution } from './memoize';
-
-describe('withoutConcurrentExecution', () => {
-    it('should memoize the function based on the cache key', async () => {
-        const fn = mock(async (_cacheKey: string, a: number, b: number) => a + b);
-        const memoized = withoutConcurrentExecution(() => null, fn);
-
-        const p1 = memoized('c1', 1, 2);
-        const p2 = memoized('c1', 1, 2);
-        const p3 = memoized('c3', 2, 3);
-
-        expect(await p1).toBe(await p2);
-        expect(await p1).not.toBe(await p3);
-        expect(fn.mock.calls.length).toBe(2);
-    });
-
-    it('should support caching per request', async () => {
-        const fn = mock(async () => Math.random());
-
-        const request1 = { id: 'request1' };
-        const request2 = { id: 'request2' };
-
-        const requestContext = new AsyncLocalStorage<{ id: string }>();
-
-        const memoized = withoutConcurrentExecution(() => requestContext.getStore(), fn);
-
-        // Both in the same request
-        const promise1 = requestContext.run(request1, () => memoized('c1'));
-        const promise2 = requestContext.run(request1, () => memoized('c1'));
-
-        // In a different request
-        const promise3 = requestContext.run(request2, () => memoized('c1'));
-
-        expect(await promise1).toBe(await promise2);
-        expect(await promise1).not.toBe(await promise3);
-        expect(fn.mock.calls.length).toBe(2);
-    });
-});
-
-describe('withCacheKey', () => {
-    it('should wrap the function by passing the cache key', async () => {
-        const fn = mock(
-            async (cacheKey: string, arg: { a: number; b: number }, c: number) =>
-                `${cacheKey}, result=${arg.a + arg.b + c}`
-        );
-        const memoized = withCacheKey(fn);
-        expect(await memoized({ a: 1, b: 2 }, 4)).toBe('[[["a",1],["b",2]],4], result=7');
-        expect(fn.mock.calls.length).toBe(1);
-    });
-});
diff --git a/packages/gitbook-v2/src/lib/data/memoize.ts b/packages/gitbook-v2/src/lib/data/memoize.ts
deleted file mode 100644
index ff1ff5e841..0000000000
--- a/packages/gitbook-v2/src/lib/data/memoize.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * Wrap a function by preventing concurrent executions of the same function.
- * With a logic to work per-request in Cloudflare Workers.
- */
-export function withoutConcurrentExecution<ArgsType extends any[], ReturnType>(
-    getGlobalContext: () => object | null | undefined,
-    wrapped: (key: string, ...args: ArgsType) => Promise<ReturnType>
-): (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> {
-    const globalPromiseCache = new WeakMap<object, Map<string, Promise<ReturnType>>>();
-
-    return (key: string, ...args: ArgsType) => {
-        const globalContext = getGlobalContext() ?? globalThis;
-
-        /**
-         * Cache storage that is scoped to the current request when executed in Cloudflare Workers,
-         * to avoid "Cannot perform I/O on behalf of a different request" errors.
-         */
-        const promiseCache =
-            globalPromiseCache.get(globalContext) ?? new Map<string, Promise<ReturnType>>();
-        globalPromiseCache.set(globalContext, promiseCache);
-
-        const concurrent = promiseCache.get(key);
-        if (concurrent) {
-            return concurrent;
-        }
-
-        const promise = (async () => {
-            try {
-                const result = await wrapped(key, ...args);
-                return result;
-            } finally {
-                promiseCache.delete(key);
-            }
-        })();
-
-        promiseCache.set(key, promise);
-
-        return promise;
-    };
-}
-
-/**
- * Wrap a function by passing it a cache key that is computed from the function arguments.
- */
-export function withCacheKey<ArgsType extends any[], ReturnType>(
-    wrapped: (cacheKey: string, ...args: ArgsType) => Promise<ReturnType>
-): (...args: ArgsType) => Promise<ReturnType> {
-    return (...args: ArgsType) => {
-        const cacheKey = getCacheKey(args);
-        return wrapped(cacheKey, ...args);
-    };
-}
-
-/**
- * Compute a cache key from the function arguments.
- */
-function getCacheKey(args: any[]) {
-    return JSON.stringify(deepSortValue(args));
-}
-
-function deepSortValue(value: unknown): unknown {
-    if (
-        typeof value === 'string' ||
-        typeof value === 'number' ||
-        typeof value === 'boolean' ||
-        value === null ||
-        value === undefined
-    ) {
-        return value;
-    }
-
-    if (Array.isArray(value)) {
-        return value.map(deepSortValue);
-    }
-
-    if (value && typeof value === 'object') {
-        return Object.entries(value)
-            .map(([key, subValue]) => {
-                return [key, deepSortValue(subValue)] as const;
-            })
-            .sort((a, b) => {
-                return a[0].localeCompare(b[0]);
-            });
-    }
-
-    return value;
-}
diff --git a/packages/gitbook-v2/src/lib/data/pages.ts b/packages/gitbook-v2/src/lib/data/pages.ts
index 4324254d9b..319c0a9170 100644
--- a/packages/gitbook-v2/src/lib/data/pages.ts
+++ b/packages/gitbook-v2/src/lib/data/pages.ts
@@ -1,15 +1,27 @@
-import type { JSONDocument, RevisionPageDocument, Space } from '@gitbook/api';
+import { isV2 } from '@/lib/v2';
+import type { JSONDocument, RevisionPageDocument } from '@gitbook/api';
+import type { GitBookSiteContext, GitBookSpaceContext } from '../context';
 import { getDataOrNull } from './errors';
-import type { GitBookDataFetcher } from './types';
 
 /**
  * Get the document for a page.
  */
 export async function getPageDocument(
-    dataFetcher: GitBookDataFetcher,
-    space: Space,
+    context: GitBookSpaceContext | GitBookSiteContext,
     page: RevisionPageDocument
 ): Promise<JSONDocument | null> {
+    const { dataFetcher, space } = context;
+
+    if (isV2()) {
+        return getDataOrNull(
+            dataFetcher.getRevisionPageDocument({
+                spaceId: space.id,
+                revisionId: space.revision,
+                pageId: page.id,
+            })
+        );
+    }
+
     if (page.documentId) {
         return getDataOrNull(
             dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId })
diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts
index 178a0ba77d..8d987047e2 100644
--- a/packages/gitbook-v2/src/lib/data/types.ts
+++ b/packages/gitbook-v2/src/lib/data/types.ts
@@ -106,6 +106,15 @@ export interface GitBookDataFetcher {
         pageId: string;
     }): Promise<DataFetcherResponse<string>>;
 
+    /**
+     * Get the document of a page by its path.
+     */
+    getRevisionPageDocument(params: {
+        spaceId: string;
+        revisionId: string;
+        pageId: string;
+    }): Promise<DataFetcherResponse<api.JSONDocument>>;
+
     /**
      * Get a document by its space ID and document ID.
      */
diff --git a/packages/gitbook-v2/src/lib/data/visitor.ts b/packages/gitbook-v2/src/lib/data/visitor.ts
index f93f0afe87..e59e32365c 100644
--- a/packages/gitbook-v2/src/lib/data/visitor.ts
+++ b/packages/gitbook-v2/src/lib/data/visitor.ts
@@ -1,6 +1,6 @@
 import { withLeadingSlash, withTrailingSlash } from '@/lib/paths';
 import type { PublishedSiteContent } from '@gitbook/api';
-import { getProxyRequestIdentifier, isProxyRequest } from './proxy';
+import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy';
 
 /**
  * Get the appropriate base path for the visitor authentication cookie.
diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts
new file mode 100644
index 0000000000..679f2c5ce7
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts
@@ -0,0 +1,81 @@
+import { describe, expect, it } from 'bun:test';
+import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL';
+
+describe('checkIsSizableImageURL', () => {
+    it('should return Skip for non-parsable URLs', () => {
+        expect(checkIsSizableImageURL('not a url')).toBe(SizableImageAction.Skip);
+    });
+
+    it('should return Skip for non-http(s) URLs', () => {
+        expect(checkIsSizableImageURL('data:image/png;base64,abc')).toBe(SizableImageAction.Skip);
+        expect(checkIsSizableImageURL('file:///path/to/image.jpg')).toBe(SizableImageAction.Skip);
+    });
+
+    it('should return Skip for localhost URLs', () => {
+        expect(checkIsSizableImageURL('http://localhost:3000/image.jpg')).toBe(
+            SizableImageAction.Skip
+        );
+        expect(checkIsSizableImageURL('https://localhost/image.png')).toBe(SizableImageAction.Skip);
+    });
+
+    it('should return Skip for GitBook image URLs', () => {
+        expect(checkIsSizableImageURL('https://example.com/~gitbook/image/test.jpg')).toBe(
+            SizableImageAction.Skip
+        );
+    });
+
+    it('should return Resize for supported image extensions', () => {
+        expect(checkIsSizableImageURL('https://example.com/image.jpg')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.jpeg')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.png')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.gif')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.webp')).toBe(
+            SizableImageAction.Resize
+        );
+    });
+
+    it('should return Resize for URLs without extensions', () => {
+        expect(checkIsSizableImageURL('https://example.com/image')).toBe(SizableImageAction.Resize);
+    });
+
+    it('should return Passthrough for unsupported image extensions', () => {
+        expect(checkIsSizableImageURL('https://example.com/image.svg')).toBe(
+            SizableImageAction.Passthrough
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.bmp')).toBe(
+            SizableImageAction.Passthrough
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.tiff')).toBe(
+            SizableImageAction.Passthrough
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.ico')).toBe(
+            SizableImageAction.Passthrough
+        );
+    });
+
+    it('should handle URLs with query parameters correctly', () => {
+        expect(checkIsSizableImageURL('https://example.com/image.jpg?width=100')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.svg?height=200')).toBe(
+            SizableImageAction.Passthrough
+        );
+    });
+
+    it('should be case-insensitive for extensions', () => {
+        expect(checkIsSizableImageURL('https://example.com/image.JPG')).toBe(
+            SizableImageAction.Resize
+        );
+        expect(checkIsSizableImageURL('https://example.com/image.PNG')).toBe(
+            SizableImageAction.Resize
+        );
+    });
+});
diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts
index 45754af505..486ea7a69a 100644
--- a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts
+++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts
@@ -1,4 +1,15 @@
-import { checkIsHttpURL } from '@/lib/urls';
+import { getExtension } from '@/lib/paths';
+
+export enum SizableImageAction {
+    Resize = 'resize',
+    Skip = 'skip',
+    Passthrough = 'passthrough',
+}
+
+/**
+ * https://developers.cloudflare.com/images/transform-images/#supported-input-formats
+ */
+const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
 
 /**
  * Check if an image URL is resizable.
@@ -6,22 +17,27 @@ import { checkIsHttpURL } from '@/lib/urls';
  * Skip it for SVGs.
  * Skip it for GitBook images (to avoid recursion).
  */
-export function checkIsSizableImageURL(input: string): boolean {
+export function checkIsSizableImageURL(input: string): SizableImageAction {
     if (!URL.canParse(input)) {
-        return false;
-    }
-
-    if (input.includes('/~gitbook/image')) {
-        return false;
+        return SizableImageAction.Skip;
     }
 
     const parsed = new URL(input);
-    if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
-        return false;
+    if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
+        return SizableImageAction.Skip;
+    }
+    if (parsed.hostname === 'localhost') {
+        return SizableImageAction.Skip;
     }
-    if (!checkIsHttpURL(parsed)) {
-        return false;
+    if (parsed.pathname.includes('/~gitbook/image')) {
+        return SizableImageAction.Skip;
+    }
+
+    const extension = getExtension(parsed.pathname).toLowerCase();
+    if (!extension || SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
+        // If no extension, we consider it resizable.
+        return SizableImageAction.Resize;
     }
 
-    return true;
+    return SizableImageAction.Passthrough;
 }
diff --git a/packages/gitbook-v2/src/lib/images/createImageResizer.ts b/packages/gitbook-v2/src/lib/images/createImageResizer.ts
index 703fdadd62..8507a7aefd 100644
--- a/packages/gitbook-v2/src/lib/images/createImageResizer.ts
+++ b/packages/gitbook-v2/src/lib/images/createImageResizer.ts
@@ -1,7 +1,7 @@
 import 'server-only';
 import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env';
 import type { GitBookLinker } from '../links';
-import { checkIsSizableImageURL } from './checkIsSizableImageURL';
+import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL';
 import { getImageSize } from './resizer';
 import { type SignatureVersion, generateImageSignature } from './signatures';
 import type { ImageResizer } from './types';
@@ -24,7 +24,7 @@ export function createImageResizer({
 
     return {
         getResizedImageURL: (urlInput) => {
-            if (!checkIsSizableImageURL(urlInput)) {
+            if (checkIsSizableImageURL(urlInput) === SizableImageAction.Skip) {
                 return null;
             }
 
@@ -64,7 +64,7 @@ export function createImageResizer({
         },
 
         getImageSize: async (input, options) => {
-            if (!checkIsSizableImageURL(input)) {
+            if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) {
                 return null;
             }
 
diff --git a/packages/gitbook-v2/src/lib/images/getImageResizingContextId.test.ts b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.test.ts
new file mode 100644
index 0000000000..eaba01663b
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'bun:test';
+import { getImageResizingContextId } from './getImageResizingContextId';
+
+describe('getImageResizingContextId', () => {
+    it('should return proxy identifier for proxy requests', () => {
+        const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world');
+        expect(getImageResizingContextId(proxyRequestURL)).toBe('sites/site_foo');
+    });
+
+    it('should return preview identifier for preview requests', () => {
+        const previewRequestURL = new URL('https://preview/site_foo/hello/world');
+        expect(getImageResizingContextId(previewRequestURL)).toBe('site_foo');
+    });
+
+    it('should return host for regular requests', () => {
+        const regularRequestURL = new URL('https://example.com/docs/foo/hello/world');
+        expect(getImageResizingContextId(regularRequestURL)).toBe('example.com');
+    });
+});
diff --git a/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts
index 82f9225262..40594e2ae2 100644
--- a/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts
+++ b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts
@@ -1,4 +1,5 @@
-import { getProxyRequestIdentifier, isProxyRequest } from '../data';
+import { getPreviewRequestIdentifier, isPreviewRequest } from '@v2/lib/preview';
+import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy';
 
 /**
  * Get the site identifier to use for image resizing for an incoming request.
@@ -8,6 +9,9 @@ export function getImageResizingContextId(url: URL): string {
     if (isProxyRequest(url)) {
         return getProxyRequestIdentifier(url);
     }
+    if (isPreviewRequest(url)) {
+        return getPreviewRequestIdentifier(url);
+    }
 
     return url.host;
 }
diff --git a/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts b/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts
index 4ed81dcd2b..8e656f3b70 100644
--- a/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts
+++ b/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts
@@ -1,7 +1,7 @@
 import 'server-only';
 import assertNever from 'assert-never';
 import { GITBOOK_IMAGE_RESIZE_MODE } from '../../env';
-import { checkIsSizableImageURL } from '../checkIsSizableImageURL';
+import { SizableImageAction, checkIsSizableImageURL } from '../checkIsSizableImageURL';
 import { resizeImageWithCDNCgi } from './cdn-cgi';
 import { resizeImageWithCFFetch } from './cf-fetch';
 import type { CloudflareImageJsonFormat, CloudflareImageOptions } from './types';
@@ -13,7 +13,7 @@ export async function getImageSize(
     input: string,
     defaultSize: Partial<CloudflareImageOptions> = {}
 ): Promise<{ width: number; height: number } | null> {
-    if (!checkIsSizableImageURL(input)) {
+    if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) {
         return null;
     }
 
@@ -48,13 +48,17 @@ export async function resizeImage(
         signal?: AbortSignal;
     }
 ): Promise<Response> {
-    const parsed = new URL(input);
-    if (parsed.protocol === 'data:') {
-        throw new Error('Cannot resize data: URLs');
+    const action = checkIsSizableImageURL(input);
+    if (action === SizableImageAction.Skip) {
+        throw new Error(
+            'Cannot resize this image, this function should have never been called on this url'
+        );
     }
 
-    if (parsed.hostname === 'localhost') {
-        throw new Error('Cannot resize localhost URLs');
+    if (action === SizableImageAction.Passthrough) {
+        return fetch(input, {
+            signal: options.signal,
+        });
     }
 
     switch (GITBOOK_IMAGE_RESIZE_MODE) {
diff --git a/packages/gitbook-v2/src/lib/images/resizer/types.ts b/packages/gitbook-v2/src/lib/images/resizer/types.ts
index 8bbe697f90..9cb370586f 100644
--- a/packages/gitbook-v2/src/lib/images/resizer/types.ts
+++ b/packages/gitbook-v2/src/lib/images/resizer/types.ts
@@ -13,7 +13,7 @@ export interface CloudflareImageJsonFormat {
  * https://developers.cloudflare.com/images/image-resizing/resize-with-workers/
  */
 export interface CloudflareImageOptions {
-    format?: 'webp' | 'avif' | 'json' | 'jpeg';
+    format?: 'webp' | 'avif' | 'json' | 'jpeg' | 'png';
     fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
     width?: number;
     height?: number;
diff --git a/packages/gitbook-v2/src/lib/links.test.ts b/packages/gitbook-v2/src/lib/links.test.ts
index 26a2464f50..f73ec5ee86 100644
--- a/packages/gitbook-v2/src/lib/links.test.ts
+++ b/packages/gitbook-v2/src/lib/links.test.ts
@@ -19,7 +19,7 @@ const siteGitBookIO = createLinker({
     siteBasePath: '/sitename/',
 });
 
-describe('toPathInContent', () => {
+describe('toPathInSpace', () => {
     it('should return the correct path', () => {
         expect(root.toPathInSpace('some/path')).toBe('/some/path');
         expect(variantInSection.toPathInSpace('some/path')).toBe('/section/variant/some/path');
@@ -29,6 +29,16 @@ describe('toPathInContent', () => {
         expect(root.toPathInSpace('/some/path')).toBe('/some/path');
         expect(variantInSection.toPathInSpace('/some/path')).toBe('/section/variant/some/path');
     });
+
+    it('should remove the trailing slash', () => {
+        expect(root.toPathInSpace('some/path/')).toBe('/some/path');
+        expect(variantInSection.toPathInSpace('some/path/')).toBe('/section/variant/some/path');
+    });
+
+    it('should not add a trailing slash', () => {
+        expect(root.toPathInSpace('')).toBe('');
+        expect(variantInSection.toPathInSpace('')).toBe('/section/variant');
+    });
 });
 
 describe('toPathInSite', () => {
@@ -36,6 +46,16 @@ describe('toPathInSite', () => {
         expect(root.toPathInSite('some/path')).toBe('/some/path');
         expect(siteGitBookIO.toPathInSite('some/path')).toBe('/sitename/some/path');
     });
+
+    it('should remove the trailing slash', () => {
+        expect(root.toPathInSite('some/path/')).toBe('/some/path');
+        expect(siteGitBookIO.toPathInSite('some/path/')).toBe('/sitename/some/path');
+    });
+
+    it('should not add a trailing slash', () => {
+        expect(root.toPathInSite('')).toBe('');
+        expect(siteGitBookIO.toPathInSite('')).toBe('/sitename');
+    });
 });
 
 describe('toRelativePathInSite', () => {
diff --git a/packages/gitbook-v2/src/lib/links.ts b/packages/gitbook-v2/src/lib/links.ts
index 857345703c..a64bda541c 100644
--- a/packages/gitbook-v2/src/lib/links.ts
+++ b/packages/gitbook-v2/src/lib/links.ts
@@ -93,6 +93,11 @@ export function createLinker(
         },
 
         toAbsoluteURL(absolutePath: string): string {
+            // If the path is already a full URL, we return it as is.
+            if (URL.canParse(absolutePath)) {
+                return absolutePath;
+            }
+
             if (!servedOn.host) {
                 return absolutePath;
             }
@@ -123,5 +128,9 @@ export function createLinker(
 function joinPaths(prefix: string, path: string): string {
     const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`;
     const suffixPath = path.startsWith('/') ? path.slice(1) : path;
-    return prefixPath + suffixPath;
+    return removeTrailingSlash(prefixPath + suffixPath);
+}
+
+function removeTrailingSlash(path: string): string {
+    return path.endsWith('/') ? path.slice(0, -1) : path;
 }
diff --git a/packages/gitbook-v2/src/lib/preview.test.ts b/packages/gitbook-v2/src/lib/preview.test.ts
new file mode 100644
index 0000000000..bbaf0402bd
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/preview.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, it } from 'bun:test';
+import { getPreviewRequestIdentifier, isPreviewRequest } from './preview';
+
+describe('isPreviewRequest', () => {
+    it('should return true for preview requests', () => {
+        const previewRequestURL = new URL('https://preview/site_foo/hello/world');
+        expect(isPreviewRequest(previewRequestURL)).toBe(true);
+    });
+
+    it('should return false for non-preview requests', () => {
+        const nonPreviewRequestURL = new URL('https://example.com/docs/foo/hello/world');
+        expect(isPreviewRequest(nonPreviewRequestURL)).toBe(false);
+    });
+});
+
+describe('getPreviewRequestIdentifier', () => {
+    it('should return the correct identifier for preview requests', () => {
+        const previewRequestURL = new URL('https://preview/site_foo/hello/world');
+        expect(getPreviewRequestIdentifier(previewRequestURL)).toBe('site_foo');
+    });
+});
diff --git a/packages/gitbook-v2/src/lib/preview.ts b/packages/gitbook-v2/src/lib/preview.ts
new file mode 100644
index 0000000000..7094d11970
--- /dev/null
+++ b/packages/gitbook-v2/src/lib/preview.ts
@@ -0,0 +1,13 @@
+/**
+ * Check if the request to the site is a preview request.
+ */
+export function isPreviewRequest(requestURL: URL): boolean {
+    return requestURL.host === 'preview';
+}
+
+export function getPreviewRequestIdentifier(requestURL: URL): string {
+    // For preview requests, we extract the site ID from the pathname
+    // e.g. https://preview/site_id/...
+    const pathname = requestURL.pathname.slice(1).split('/');
+    return pathname[0];
+}
diff --git a/packages/gitbook-v2/src/lib/data/proxy.test.ts b/packages/gitbook-v2/src/lib/proxy.test.ts
similarity index 100%
rename from packages/gitbook-v2/src/lib/data/proxy.test.ts
rename to packages/gitbook-v2/src/lib/proxy.test.ts
diff --git a/packages/gitbook-v2/src/lib/data/proxy.ts b/packages/gitbook-v2/src/lib/proxy.ts
similarity index 100%
rename from packages/gitbook-v2/src/lib/data/proxy.ts
rename to packages/gitbook-v2/src/lib/proxy.ts
diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts
index dd983b536a..4bcbf77aea 100644
--- a/packages/gitbook-v2/src/middleware.ts
+++ b/packages/gitbook-v2/src/middleware.ts
@@ -10,14 +10,14 @@ import {
     type ResponseCookies,
     getPathScopedCookieName,
     getResponseCookiesForVisitorAuth,
-    getVisitorToken,
+    getVisitorData,
     normalizeVisitorAuthURL,
-} from '@/lib/visitor-token';
+} from '@/lib/visitors';
 import { serveResizedImage } from '@/routes/image';
 import {
     DataFetcherError,
-    getPublishedContentByURL,
     getVisitorAuthBasePath,
+    lookupPublishedContentByUrl,
     normalizeURL,
     throwIfDataError,
 } from '@v2/lib/data';
@@ -85,17 +85,19 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
     //
     // Detect and extract the visitor authentication token from the request
     //
-    // @ts-ignore - request typing
-    const visitorToken = getVisitorToken({
+    const { visitorToken, unsignedClaims, visitorParamsCookie } = getVisitorData({
         cookies: request.cookies.getAll(),
         url: siteRequestURL,
     });
 
     const withAPIToken = async (apiToken: string | null) => {
         const siteURLData = await throwIfDataError(
-            getPublishedContentByURL({
+            lookupPublishedContentByUrl({
                 url: siteRequestURL.toString(),
-                visitorAuthToken: visitorToken?.token ?? null,
+                visitorPayload: {
+                    jwtToken: visitorToken?.token ?? undefined,
+                    unsignedClaims,
+                },
                 // When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
                 // redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
                 // For example when the VA backend signature has changed but the token stored in the cookie is not yet expired.
@@ -106,7 +108,13 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
                 apiToken,
             })
         );
-        const cookies: ResponseCookies = [];
+
+        const cookies: ResponseCookies = visitorParamsCookie
+            ? [
+                  // If visitor.* params were passed to the site URL, include a session cookie to persist these params across navigation.
+                  visitorParamsCookie,
+              ]
+            : [];
 
         //
         // Handle redirects
@@ -205,7 +213,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
         const customization = siteRequestURL.searchParams.get('customization');
         if (customization && validateSerializedCustomization(customization)) {
             routeType = 'dynamic';
-            requestHeaders.set(MiddlewareHeaders.Customization, customization);
+            // We need to encode the customization headers, otherwise it will fail for some customization values containing non ASCII chars on vercel.
+            requestHeaders.set(MiddlewareHeaders.Customization, encodeURIComponent(customization));
         }
         const theme = siteRequestURL.searchParams.get('theme');
         if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
@@ -250,6 +259,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
         console.log(`rewriting ${request.nextUrl.toString()} to ${route}`);
 
         const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString());
+        rewrittenURL.search = request.nextUrl.search; // Preserve the original search params
+
         const response = NextResponse.rewrite(rewrittenURL, {
             request: {
                 headers: requestHeaders,
diff --git a/packages/gitbook-v2/src/pages/api/~gitbook/force-revalidate.ts b/packages/gitbook-v2/src/pages/api/~gitbook/force-revalidate.ts
new file mode 100644
index 0000000000..45e6c7eca1
--- /dev/null
+++ b/packages/gitbook-v2/src/pages/api/~gitbook/force-revalidate.ts
@@ -0,0 +1,49 @@
+import crypto from 'node:crypto';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+interface JsonBody {
+    // The paths need to be the rewritten one, `res.revalidate` call don't go through the middleware
+    paths: string[];
+}
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+    // Only allow POST requests
+    if (req.method !== 'POST') {
+        return res.status(405).json({ error: 'Method not allowed' });
+    }
+    const signatureHeader = req.headers['x-gitbook-signature'] as string | undefined;
+    if (!signatureHeader) {
+        return res.status(400).json({ error: 'Missing signature header' });
+    }
+    // We cannot use env from `@/v2/lib/env` here as it make it crash because of the import "server-only" in the file.
+    if (process.env.GITBOOK_SECRET) {
+        try {
+            const computedSignature = crypto
+                .createHmac('sha256', process.env.GITBOOK_SECRET)
+                .update(JSON.stringify(req.body))
+                .digest('hex');
+
+            if (computedSignature === signatureHeader) {
+                const results = await Promise.allSettled(
+                    (req.body as JsonBody).paths.map((path) => {
+                        // biome-ignore lint/suspicious/noConsole: we want to log here
+                        console.log(`Revalidating path: ${path}`);
+                        return res.revalidate(path);
+                    })
+                );
+                return res.status(200).json({
+                    success: results.every((result) => result.status === 'fulfilled'),
+                    errors: results
+                        .filter((result) => result.status === 'rejected')
+                        .map((result) => (result as PromiseRejectedResult).reason),
+                });
+            }
+            return res.status(401).json({ error: 'Invalid signature' });
+        } catch (error) {
+            console.error('Error during revalidation:', error);
+            return res.status(400).json({ error: 'Invalid request or unable to parse JSON' });
+        }
+    }
+    // If no secret is set, we do not allow revalidation
+    return res.status(403).json({ error: 'Revalidation is disabled' });
+}
diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc
index e316e2a242..6d9c2b2324 100644
--- a/packages/gitbook-v2/wrangler.jsonc
+++ b/packages/gitbook-v2/wrangler.jsonc
@@ -2,7 +2,11 @@
     "main": ".open-next/worker.js",
     "name": "gitbook-open-v2",
     "compatibility_date": "2025-04-14",
-    "compatibility_flags": ["nodejs_compat", "allow_importable_env"],
+    "compatibility_flags": [
+        "nodejs_compat",
+        "allow_importable_env",
+        "global_fetch_strictly_public"
+    ],
     "assets": {
         "directory": ".open-next/assets",
         "binding": "ASSETS"
@@ -10,8 +14,15 @@
     "observability": {
         "enabled": true
     },
+    "vars": {
+        "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
+        "IS_PREVIEW": "false"
+    },
     "env": {
         "preview": {
+            "vars": {
+                "IS_PREVIEW": "true"
+            },
             "r2_buckets": [
                 {
                     "binding": "NEXT_INC_CACHE_R2_BUCKET",
@@ -83,6 +94,14 @@
             ]
         },
         "production": {
+            "vars": {
+                // This is a bit misleading, but it means that we can have 500 concurrent revalidations
+                // This means that we'll have up to 100 durable objects instance running at the same time
+                "MAX_REVALIDATE_CONCURRENCY": "100",
+                // Temporary variable to find the issue once deployed
+                // TODO: remove this once the issue is fixed
+                "DEBUG_CLOUDFLARE": "true"
+            },
             "routes": [
                 {
                     "pattern": "open-2c.gitbook.com/*",
diff --git a/packages/gitbook/CHANGELOG.md b/packages/gitbook/CHANGELOG.md
index dc285100ff..fb855a6305 100644
--- a/packages/gitbook/CHANGELOG.md
+++ b/packages/gitbook/CHANGELOG.md
@@ -1,5 +1,51 @@
 # gitbook
 
+## 0.12.0
+
+### Minor Changes
+
+- 8339e91: Fix images in reusable content across spaces.
+- 326e28e: Design tweaks to code blocks and OpenAPI pages
+- 3119066: Add support for reusable content across spaces.
+- 7d7806d: Pass SVG images through image resizing without resizing them to serve them from optimal host.
+
+### Patch Changes
+
+- c4ebb3f: Fix openapi-select hover in responses
+- aed79fd: Decrease rounding of header logo
+- 42ca7e1: Fix openapi CR preview
+- e6ddc0f: Fix URL in sitemap
+- 5e975ab: Fix code highlighting for HTTP
+- 5d504ff: Fix resolution of links in reusable contents
+- 95a1f65: Better print layouts: wrap code blocks & force table column auto-sizing
+- 0499966: Fix invalid sitemap.xml generated with relative URLs instead of absolute ones
+- 2a805cc: Change OpenAPI schema-optional from `info` to `tint` color
+- 580101d: Fix schemas disclosure label causing client error
+- 12a455d: Fix OpenAPI layout issues
+- 97b7c79: Increase logging around caching behaviour causing page crashes.
+- 373f18f: Prevent section group popovers from opening on click
+- 3f29206: Update the regex for validating site redirect
+- 0c973a3: Always link main logo to the root of the site
+- ae5f1ab: Change `Dropdown`s to use Radix's `DropdownMenu`
+- 0e201d5: Add border to filled sidebar on gradient theme
+- dd043df: Revert investigation work around URL caches.
+- 89a5816: Fix OpenAPI disclosure label ("Show properties") misalignment on mobile
+- Updated dependencies [c3f6b8c]
+- Updated dependencies [d00dc8c]
+- Updated dependencies [42ca7e1]
+- Updated dependencies [326e28e]
+- Updated dependencies [5e975ab]
+- Updated dependencies [f7a3470]
+- Updated dependencies [580101d]
+- Updated dependencies [20ebecb]
+- Updated dependencies [80cb52a]
+- Updated dependencies [cb5598d]
+- Updated dependencies [c6637b0]
+- Updated dependencies [a3ec264]
+  - @gitbook/colors@0.3.3
+  - @gitbook/openapi-parser@2.1.4
+  - @gitbook/react-openapi@1.3.0
+
 ## 0.11.1
 
 ### Patch Changes
@@ -564,7 +610,7 @@
 - 4cbcc5b: Rollback of scalar modal while fixing perf issue
 - 3996110: Optimize images rendered in community ads
 - 133c3e7: Update design of Checkbox to be more consistent and readable
-- 5096f7f: Disable KV cache for docs.gitbook.com as a test, also disable it for change-request to improve consistency
+- 5096f7f: Disable KV cache for gitbook.com/docs as a test, also disable it for change-request to improve consistency
 - 0f1565c: Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
 - 2ff7ed1: Fix table of contents being visible on mobile when disabled at the page level
 - b075f0f: Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected`
diff --git a/packages/gitbook/e2e/customers.spec.ts b/packages/gitbook/e2e/customers.spec.ts
index f0612ba8e0..aa8e6e4a7c 100644
--- a/packages/gitbook/e2e/customers.spec.ts
+++ b/packages/gitbook/e2e/customers.spec.ts
@@ -204,11 +204,11 @@ const testCases: TestsCase[] = [
         contentBaseURL: 'https://sosovalue-white-paper.gitbook.io',
         tests: [{ name: 'Home', url: '/' }],
     },
-    {
-        name: 'docs.revrobotics.com',
-        contentBaseURL: 'https://docs.revrobotics.com',
-        tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
-    },
+    // {
+    //     name: 'docs.revrobotics.com',
+    //     contentBaseURL: 'https://docs.revrobotics.com',
+    //     tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
+    // },
     {
         name: 'chartschool.stockcharts.com',
         contentBaseURL: 'https://chartschool.stockcharts.com',
diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts
index e7875cd4e8..3a884ae5ea 100644
--- a/packages/gitbook/e2e/internal.spec.ts
+++ b/packages/gitbook/e2e/internal.spec.ts
@@ -1,9 +1,11 @@
 import {
     CustomizationBackground,
     CustomizationCorners,
+    CustomizationDepth,
     CustomizationHeaderPreset,
     CustomizationIconsStyle,
     CustomizationSidebarListStyle,
+    CustomizationThemeMode,
 } from '@gitbook/api';
 import { expect } from '@playwright/test';
 import jwt from 'jsonwebtoken';
@@ -12,7 +14,7 @@ import {
     VISITOR_TOKEN_COOKIE,
     getVisitorAuthCookieName,
     getVisitorAuthCookieValue,
-} from '@/lib/visitor-token';
+} from '@/lib/visitors';
 
 import { getSiteAPIToken } from '../tests/utils';
 import {
@@ -111,24 +113,24 @@ const testCases: TestsCase[] = [
                 name: 'Customized variant titles are displayed',
                 url: '',
                 run: async (page) => {
-                    const spaceDrowpdown = page
+                    const spaceDropdown = page
                         .locator('[data-testid="space-dropdown-button"]')
                         .locator('visible=true');
-                    await spaceDrowpdown.click();
+                    await spaceDropdown.click();
 
                     const variantSelectionDropdown = page.locator(
-                        'css=[data-testid="space-dropdown-button"] + div'
+                        'css=[data-testid="dropdown-menu"]'
                     );
                     // the customized space title
                     await expect(
-                        variantSelectionDropdown.getByRole('link', {
+                        variantSelectionDropdown.getByRole('menuitem', {
                             name: 'Multi-Variants',
                         })
                     ).toBeVisible();
 
                     // the NON-customized space title
                     await expect(
-                        variantSelectionDropdown.getByRole('link', {
+                        variantSelectionDropdown.getByRole('menuitem', {
                             name: 'RFCs',
                         })
                     ).toBeVisible();
@@ -145,14 +147,17 @@ const testCases: TestsCase[] = [
                 url: 'api-multi-versions/reference/api-reference/pets',
                 screenshot: false,
                 run: async (page) => {
-                    const spaceDrowpdown = await page
+                    const spaceDropdown = await page
                         .locator('[data-testid="space-dropdown-button"]')
                         .locator('visible=true');
-                    await spaceDrowpdown.click();
+                    await spaceDropdown.click();
 
+                    const variantSelectionDropdown = page.locator(
+                        'css=[data-testid="dropdown-menu"]'
+                    );
                     // Click the second variant in the dropdown
-                    await page
-                        .getByRole('link', {
+                    await variantSelectionDropdown
+                        .getByRole('menuitem', {
                             name: '2.0',
                         })
                         .click();
@@ -168,14 +173,18 @@ const testCases: TestsCase[] = [
                 url: 'api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/reference/api-reference/pets',
                 screenshot: false,
                 run: async (page) => {
-                    const spaceDrowpdown = await page
+                    const spaceDropdown = await page
                         .locator('[data-testid="space-dropdown-button"]')
                         .locator('visible=true');
-                    await spaceDrowpdown.click();
+                    await spaceDropdown.click();
+
+                    const variantSelectionDropdown = page.locator(
+                        'css=[data-testid="dropdown-menu"]'
+                    );
 
                     // Click the second variant in the dropdown
-                    await page
-                        .getByRole('link', {
+                    await variantSelectionDropdown
+                        .getByRole('menuitem', {
                             name: '2.0',
                         })
                         .click();
@@ -205,14 +214,18 @@ const testCases: TestsCase[] = [
                     return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`;
                 },
                 run: async (page) => {
-                    const spaceDrowpdown = await page
+                    const spaceDropdown = await page
                         .locator('[data-testid="space-dropdown-button"]')
                         .locator('visible=true');
-                    await spaceDrowpdown.click();
+                    await spaceDropdown.click();
+
+                    const variantSelectionDropdown = page.locator(
+                        'css=[data-testid="dropdown-menu"]'
+                    );
 
                     // Click the second variant in the dropdown
-                    await page
-                        .getByRole('link', {
+                    await variantSelectionDropdown
+                        .getByRole('menuitem', {
                             name: '2.0',
                         })
                         .click();
@@ -258,7 +271,7 @@ const testCases: TestsCase[] = [
     },
     {
         name: 'GitBook',
-        contentBaseURL: 'https://docs.gitbook.com',
+        contentBaseURL: 'https://gitbook.com/docs/',
         tests: [
             {
                 name: 'Home',
@@ -419,6 +432,38 @@ const testCases: TestsCase[] = [
             },
         ],
     },
+    {
+        name: 'llms.txt',
+        skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
+        contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
+        tests: [
+            {
+                name: 'llms.txt',
+                url: 'llms.txt',
+                screenshot: false,
+                run: async (_page, response) => {
+                    expect(response?.status()).toBe(200);
+                    expect(response?.headers()['content-type']).toContain('text/markdown');
+                },
+            },
+        ],
+    },
+    {
+        name: 'llms-full.txt',
+        skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
+        contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
+        tests: [
+            {
+                name: 'llms-full.txt',
+                url: 'llms-full.txt',
+                screenshot: false,
+                run: async (_page, response) => {
+                    expect(response?.status()).toBe(200);
+                    expect(response?.headers()['content-type']).toContain('text/markdown');
+                },
+            },
+        ],
+    },
     {
         name: 'Site subdirectory (proxy)',
         skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
@@ -558,6 +603,11 @@ const testCases: TestsCase[] = [
                 url: 'blocks/emojis',
                 run: waitForCookiesDialog,
             },
+            {
+                name: 'Icons',
+                url: 'blocks/icons',
+                run: waitForCookiesDialog,
+            },
             {
                 name: 'Links',
                 url: 'blocks/links',
@@ -621,6 +671,7 @@ const testCases: TestsCase[] = [
                 name: 'Stepper',
                 url: 'blocks/stepper',
             },
+            { name: 'Columns', url: 'blocks/columns' },
         ],
     },
     {
@@ -637,6 +688,16 @@ const testCases: TestsCase[] = [
                 url: 'page-options/page-with-cover',
                 run: waitForCookiesDialog,
             },
+            {
+                name: 'With cover for dark mode',
+                url: `page-options/page-with-dark-cover${getCustomizationURL({
+                    themes: {
+                        default: CustomizationThemeMode.Dark,
+                        toggeable: false,
+                    },
+                })}`,
+                run: waitForCookiesDialog,
+            },
             {
                 name: 'With hero cover',
                 url: 'page-options/page-with-hero-cover',
@@ -806,6 +867,16 @@ const testCases: TestsCase[] = [
                 }),
                 run: waitForCookiesDialog,
             },
+            {
+                name: `With flat and circular corners - Theme mode ${themeMode}`,
+                url: getCustomizationURL({
+                    styling: {
+                        depth: CustomizationDepth.Flat,
+                        corners: CustomizationCorners.Circular,
+                    },
+                }),
+                run: waitForCookiesDialog,
+            },
         ]),
     },
     {
diff --git a/packages/gitbook/e2e/pdf.spec.ts b/packages/gitbook/e2e/pdf.spec.ts
new file mode 100644
index 0000000000..4798a2a047
--- /dev/null
+++ b/packages/gitbook/e2e/pdf.spec.ts
@@ -0,0 +1,170 @@
+import { argosScreenshot } from '@argos-ci/playwright';
+import { expect, test } from '@playwright/test';
+import { getContentTestURL } from '../tests/utils';
+import { waitForIcons } from './util';
+
+test.describe('PDF export', () => {
+    test('export all pages as PDF (e2e)', async ({ page }) => {
+        // Set the header to disable the Vercel toolbar
+        // But only on the main document as it'd cause CORS issues on other resources
+        await page.route('**/*', async (route, request) => {
+            if (request.resourceType() === 'document') {
+                await route.continue({
+                    headers: {
+                        ...request.headers(),
+                        'x-vercel-skip-toolbar': '1',
+                    },
+                });
+            } else {
+                await route.continue();
+            }
+        });
+
+        await page.goto(
+            getContentTestURL(
+                'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?limit=10'
+            )
+        );
+
+        const printBtn = page.getByTestId('print-button');
+        await expect(printBtn).toBeVisible();
+
+        await argosScreenshot(page, 'pdf - all pages', {
+            viewports: ['macbook-13'],
+            argosCSS: `
+                            /* Hide Intercom */
+                            .intercom-lightweight-app {
+                                display: none !important;
+                            }
+                            `,
+            threshold: undefined,
+            fullPage: true,
+            beforeScreenshot: async ({ runStabilization }) => {
+                await runStabilization();
+                await waitForIcons(page);
+            },
+        });
+    });
+
+    test('export all pages as PDF (GitBook docs)', async ({ page }) => {
+        // Set the header to disable the Vercel toolbar
+        // But only on the main document as it'd cause CORS issues on other resources
+        await page.route('**/*', async (route, request) => {
+            if (request.resourceType() === 'document') {
+                await route.continue({
+                    headers: {
+                        ...request.headers(),
+                        'x-vercel-skip-toolbar': '1',
+                    },
+                });
+            } else {
+                await route.continue();
+            }
+        });
+
+        await page.goto(getContentTestURL('https://gitbook.com/docs/~gitbook/pdf?limit=10'));
+
+        const printBtn = page.getByTestId('print-button');
+        await expect(printBtn).toBeVisible();
+
+        await argosScreenshot(page, 'pdf - all pages', {
+            viewports: ['macbook-13'],
+            argosCSS: `
+                            /* Hide Intercom */
+                            .intercom-lightweight-app {
+                                display: none !important;
+                            }
+                            `,
+            threshold: undefined,
+            fullPage: true,
+            beforeScreenshot: async ({ runStabilization }) => {
+                await runStabilization();
+                await waitForIcons(page);
+            },
+        });
+    });
+
+    test('export a single page as PDF (e2e)', async ({ page }) => {
+        // Set the header to disable the Vercel toolbar
+        // But only on the main document as it'd cause CORS issues on other resources
+        await page.route('**/*', async (route, request) => {
+            if (request.resourceType() === 'document') {
+                await route.continue({
+                    headers: {
+                        ...request.headers(),
+                        'x-vercel-skip-toolbar': '1',
+                    },
+                });
+            } else {
+                await route.continue();
+            }
+        });
+
+        await page.goto(
+            getContentTestURL(
+                'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?page=Bw7LjWwgTjV8nIV4s7rs&only=yes&limit=2'
+            )
+        );
+
+        const printBtn = page.getByTestId('print-button');
+        await expect(printBtn).toBeVisible();
+
+        await argosScreenshot(page, 'pdf - all pages', {
+            viewports: ['macbook-13'],
+            argosCSS: `
+                            /* Hide Intercom */
+                            .intercom-lightweight-app {
+                                display: none !important;
+                            }
+                            `,
+            threshold: undefined,
+            fullPage: true,
+            beforeScreenshot: async ({ runStabilization }) => {
+                await runStabilization();
+                await waitForIcons(page);
+            },
+        });
+    });
+
+    test('export a single page as PDF (GitBook docs)', async ({ page }) => {
+        // Set the header to disable the Vercel toolbar
+        // But only on the main document as it'd cause CORS issues on other resources
+        await page.route('**/*', async (route, request) => {
+            if (request.resourceType() === 'document') {
+                await route.continue({
+                    headers: {
+                        ...request.headers(),
+                        'x-vercel-skip-toolbar': '1',
+                    },
+                });
+            } else {
+                await route.continue();
+            }
+        });
+
+        await page.goto(
+            getContentTestURL(
+                'https://gitbook.com/docs/~gitbook/pdf?page=DfnNkU49mvLe2ythHAyx&only=yes&limit=2'
+            )
+        );
+
+        const printBtn = page.getByTestId('print-button');
+        await expect(printBtn).toBeVisible();
+
+        await argosScreenshot(page, 'pdf - all pages', {
+            viewports: ['macbook-13'],
+            argosCSS: `
+                            /* Hide Intercom */
+                            .intercom-lightweight-app {
+                                display: none !important;
+                            }
+                            `,
+            threshold: undefined,
+            fullPage: true,
+            beforeScreenshot: async ({ runStabilization }) => {
+                await runStabilization();
+                await waitForIcons(page);
+            },
+        });
+    });
+});
diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts
index c2edad6186..937b776d70 100644
--- a/packages/gitbook/e2e/util.ts
+++ b/packages/gitbook/e2e/util.ts
@@ -3,6 +3,7 @@ import {
     CustomizationBackground,
     CustomizationCorners,
     CustomizationDefaultFont,
+    CustomizationDepth,
     type CustomizationHeaderItem,
     CustomizationHeaderPreset,
     CustomizationIconsStyle,
@@ -168,6 +169,7 @@ export function runTestCases(testCases: TestsCase[]) {
                               new URL(testEntryPathname, testCase.contentBaseURL).toString()
                           )
                         : getTestURL(testEntryPathname);
+
                     if (testEntry.cookies) {
                         await context.addCookies(
                             testEntry.cookies.map((cookie) => ({
@@ -277,6 +279,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
             dangerColor: { light: '#FB2C36', dark: '#FB2C36' },
             successColor: { light: '#00C950', dark: '#00C950' },
             corners: CustomizationCorners.Rounded,
+            depth: CustomizationDepth.Subtle,
             font: CustomizationDefaultFont.Inter,
             background: CustomizationBackground.Plain,
             icons: CustomizationIconsStyle.Regular,
@@ -343,21 +346,32 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
 /**
  * Wait for all icons present on the page to be loaded.
  */
-async function waitForIcons(page: Page) {
+export async function waitForIcons(page: Page) {
     await page.waitForFunction(() => {
-        const urlStates: Record<string, 'pending' | 'loaded'> =
-            (window as any).__ICONS_STATES__ || {};
+        const urlStates: Record<
+            string,
+            { state: 'pending'; uri: null } | { state: 'loaded'; uri: string }
+        > = (window as any).__ICONS_STATES__ || {};
         (window as any).__ICONS_STATES__ = urlStates;
 
+        const fetchSvgAsDataUri = async (url: string): Promise<string> => {
+            const response = await fetch(url);
+            if (!response.ok) {
+                throw new Error(`Failed to fetch SVG: ${response.status}`);
+            }
+
+            const svgText = await response.text();
+            const encoded = encodeURIComponent(svgText).replace(/'/g, '%27').replace(/"/g, '%22');
+
+            return `data:image/svg+xml;charset=utf-8,${encoded}`;
+        };
+
         const loadUrl = (url: string) => {
             // Mark the URL as pending.
-            urlStates[url] = 'pending';
-
-            const img = new Image();
-            img.onload = () => {
-                urlStates[url] = 'loaded';
-            };
-            img.src = url;
+            urlStates[url] = { state: 'pending', uri: null };
+            fetchSvgAsDataUri(url).then((uri) => {
+                urlStates[url] = { state: 'loaded', uri };
+            });
         };
 
         const icons = Array.from(document.querySelectorAll('svg.gb-icon'));
@@ -393,18 +407,11 @@ async function waitForIcons(page: Page) {
 
             // If the URL is already queued for loading, we return the state.
             if (urlStates[url]) {
-                if (urlStates[url] === 'loaded') {
+                if (urlStates[url].state === 'loaded') {
                     icon.setAttribute('data-argos-state', 'pending');
-                    const bckMaskImage = icon.style.maskImage;
-                    const bckDisplay = icon.style.display;
-                    icon.style.maskImage = '';
-                    icon.style.display = 'none';
+                    icon.style.maskImage = `url("${urlStates[url].uri}")`;
                     requestAnimationFrame(() => {
-                        icon.style.maskImage = bckMaskImage;
-                        icon.style.display = bckDisplay;
-                        requestAnimationFrame(() => {
-                            icon.setAttribute('data-argos-state', 'loaded');
-                        });
+                        icon.setAttribute('data-argos-state', 'loaded');
                     });
                     return false;
                 }
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index d0abc7512b..a430218cfb 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -1,6 +1,6 @@
 {
     "name": "gitbook",
-    "version": "0.11.1",
+    "version": "0.12.0",
     "private": true,
     "scripts": {
         "dev": "env-cmd --silent -f ../../.env.local next dev",
@@ -8,25 +8,27 @@
         "build:cloudflare": "next-on-pages --custom-entrypoint=./src/cloudflare-entrypoint.ts",
         "start": "next start",
         "typecheck": "tsc --noEmit",
-        "e2e": "playwright test e2e/internal.spec.ts",
+        "e2e": "playwright test e2e/internal.spec.ts e2e/pdf.spec.ts",
         "e2e-customers": "playwright test e2e/customers.spec.ts",
-        "unit": "bun test {src,packages}",
+        "unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
         "generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math",
         "copy:icons": "gitbook-icons ./public/~gitbook/static/icons",
         "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
     },
     "dependencies": {
-        "@gitbook/api": "*",
+        "@gitbook/api": "catalog:",
         "@gitbook/cache-do": "workspace:*",
         "@gitbook/cache-tags": "workspace:*",
         "@gitbook/colors": "workspace:*",
         "@gitbook/emoji-codepoints": "workspace:*",
         "@gitbook/icons": "workspace:*",
+        "@gitbook/fonts": "workspace:*",
         "@gitbook/openapi-parser": "workspace:*",
         "@gitbook/react-contentkit": "workspace:*",
         "@gitbook/react-math": "workspace:*",
         "@gitbook/react-openapi": "workspace:*",
         "@radix-ui/react-checkbox": "^1.0.4",
+        "@radix-ui/react-dropdown-menu": "^2.1.12",
         "@radix-ui/react-navigation-menu": "^1.2.3",
         "@radix-ui/react-popover": "^1.0.7",
         "@radix-ui/react-tooltip": "^1.1.8",
@@ -37,6 +39,7 @@
         "assert-never": "^1.2.1",
         "bun-types": "^1.1.20",
         "classnames": "^2.5.1",
+        "event-iterator": "^2.0.0",
         "framer-motion": "^10.16.14",
         "js-cookie": "^3.0.5",
         "jsontoxml": "^1.0.1",
@@ -44,16 +47,24 @@
         "katex": "^0.16.9",
         "mathjax": "^3.2.2",
         "mdast-util-to-markdown": "^2.1.2",
+        "mdast-util-from-markdown": "^2.0.2",
+        "mdast-util-frontmatter": "^2.0.1",
+        "mdast-util-gfm": "^3.1.0",
+        "micromark-extension-gfm": "^3.0.0",
+        "micromark-extension-frontmatter": "^2.0.0",
+        "unist-util-remove": "^4.0.0",
+        "unist-util-visit": "^5.0.0",
         "memoizee": "^0.4.17",
         "next": "14.2.26",
         "next-themes": "^0.2.1",
         "nuqs": "^2.2.3",
         "object-hash": "^3.0.0",
         "openapi-types": "^12.1.3",
-        "p-map": "^7.0.0",
+        "p-map": "^7.0.3",
         "parse-cache-control": "^1.0.1",
-        "react": "18.3.1",
-        "react-dom": "18.3.1",
+        "partial-json": "^0.1.7",
+        "react": "^19.0.0",
+        "react-dom": "^19.0.0",
         "react-hotkeys-hook": "^4.4.1",
         "rehype-sanitize": "^6.0.0",
         "rehype-stringify": "^10.0.1",
@@ -70,13 +81,13 @@
         "usehooks-ts": "^3.1.0",
         "zod": "^3.24.2",
         "zod-to-json-schema": "^3.24.5",
-        "event-iterator": "^2.0.0",
-        "partial-json": "^0.1.7",
-        "zustand": "^5.0.3"
+        "zustand": "^5.0.3",
+        "image-size": "^2.0.2",
+        "direction": "^2.0.1"
     },
     "devDependencies": {
         "@argos-ci/playwright": "^5.0.3",
-        "@cloudflare/next-on-pages": "1.13.7",
+        "@cloudflare/next-on-pages": "1.13.12",
         "@cloudflare/workers-types": "^4.20241230.0",
         "@playwright/test": "^1.51.1",
         "@types/js-cookie": "^3.0.6",
diff --git a/packages/gitbook/src/components/Ads/AdClassicRendering.tsx b/packages/gitbook/src/components/Ads/AdClassicRendering.tsx
index e6d263bf6a..82613af491 100644
--- a/packages/gitbook/src/components/Ads/AdClassicRendering.tsx
+++ b/packages/gitbook/src/components/Ads/AdClassicRendering.tsx
@@ -19,14 +19,14 @@ export async function AdClassicRendering({
     insightsAd: SiteInsightsAd | null;
     context: GitBookBaseContext;
 }) {
-    const smallImgSrc =
+    const [smallImgSrc, logoSrc] = await Promise.all([
         'smallImage' in ad
-            ? await getResizedImageURL(context.imageResizer, ad.smallImage, { width: 192, dpr: 2 })
-            : null;
-    const logoSrc =
+            ? getResizedImageURL(context.imageResizer, ad.smallImage, { width: 192, dpr: 2 })
+            : null,
         'logo' in ad
-            ? await getResizedImageURL(context.imageResizer, ad.logo, { width: 192 - 48, dpr: 2 })
-            : null;
+            ? getResizedImageURL(context.imageResizer, ad.logo, { width: 192 - 48, dpr: 2 })
+            : null,
+    ]);
     return (
         <Link
             rel="sponsored noopener"
diff --git a/packages/gitbook/src/components/Ads/renderAd.tsx b/packages/gitbook/src/components/Ads/renderAd.tsx
index 146f0d2530..96ff7bd3ba 100644
--- a/packages/gitbook/src/components/Ads/renderAd.tsx
+++ b/packages/gitbook/src/components/Ads/renderAd.tsx
@@ -43,10 +43,12 @@ interface FetchPlaceholderAdOptions {
  * and properly access user-agent and IP.
  */
 export async function renderAd(options: FetchAdOptions) {
-    const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
+    const [context, result] = await Promise.all([
+        isV2() ? getServerActionBaseContext() : getV1BaseContext(),
+        options.source === 'live' ? fetchAd(options) : getPlaceholderAd(),
+    ]);
 
     const mode = options.source === 'live' ? options.mode : 'classic';
-    const result = options.source === 'live' ? await fetchAd(options) : await getPlaceholderAd();
     if (!result || !result.ad.description || !result.ad.statlink) {
         return null;
     }
diff --git a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
index c89bb24804..decd177a13 100644
--- a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
+++ b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
@@ -1,12 +1,14 @@
 'use client';
 
+import { tString, useLanguage } from '@/intl/client';
 import * as storage from '@/lib/local-storage';
 import type { ResolvedContentRef } from '@/lib/references';
 import { tcls } from '@/lib/tailwind';
 import { type CustomizationAnnouncement, SiteInsightsLinkPosition } from '@gitbook/api';
 import { Icon, type IconName } from '@gitbook/icons';
 import { CONTAINER_STYLE } from '../layout';
-import { Link, linkStyles } from '../primitives';
+import { Link } from '../primitives';
+import { LinkStyles } from '../primitives/styles';
 import { ANNOUNCEMENT_CSS_CLASS, ANNOUNCEMENT_STORAGE_KEY } from './constants';
 
 /**
@@ -18,6 +20,8 @@ export function AnnouncementBanner(props: {
 }) {
     const { announcement, contentRef } = props;
 
+    const language = useLanguage();
+
     const hasLink = announcement.link && contentRef?.href;
     const closeable = announcement.style !== 'danger';
 
@@ -25,13 +29,13 @@ export function AnnouncementBanner(props: {
     const style = BANNER_STYLES[announcement.style];
 
     return (
-        <div className="announcement-banner theme-bold:bg-header-background pt-4 pb-2">
+        <div id="announcement-banner" className="theme-bold:bg-header-background pt-4 pb-2">
             <div className="scroll-nojump">
                 <div className={tcls('relative', CONTAINER_STYLE)}>
                     <Tag
                         href={contentRef?.href ?? ''}
                         className={tcls(
-                            'flex w-full items-start justify-center overflow-hidden rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
+                            'flex w-full items-start justify-center overflow-hidden circular-corners:rounded-xl rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
                             style.container,
                             closeable && 'pr-12',
                             hasLink && style.hover
@@ -55,7 +59,7 @@ export function AnnouncementBanner(props: {
                         <div>
                             {announcement.message}
                             {hasLink ? (
-                                <div className={tcls(linkStyles, style.link, 'ml-1 inline')}>
+                                <div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
                                     {contentRef?.icon ? (
                                         <span className="mr-1 ml-2 *:inline">
                                             {contentRef?.icon}
@@ -78,9 +82,10 @@ export function AnnouncementBanner(props: {
                     </Tag>
                     {closeable ? (
                         <button
-                            className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
+                            className={`absolute top-0 right-4 mt-2 mr-2 rounded circular-corners:rounded-lg straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
                             type="button"
                             onClick={dismissAnnouncement}
+                            title={tString(language, 'close')}
                         >
                             <Icon icon="close" className="size-4" />
                         </button>
diff --git a/packages/gitbook/src/components/Cookies/CookiesToast.tsx b/packages/gitbook/src/components/Cookies/CookiesToast.tsx
index 13dc7605d0..281e6ad911 100644
--- a/packages/gitbook/src/components/Cookies/CookiesToast.tsx
+++ b/packages/gitbook/src/components/Cookies/CookiesToast.tsx
@@ -47,9 +47,11 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
                 'bg-tint-base',
                 'rounded',
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-2xl',
                 'ring-1',
                 'ring-tint-subtle',
                 'shadow-1xs',
+                'depth-flat:shadow-none',
                 'p-4',
                 'pr-8',
                 'bottom-4',
@@ -72,7 +74,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
             <button
                 type="button"
                 onClick={() => setShow(false)}
-                aria-label={tString(language, 'cookies_close')}
+                aria-label={tString(language, 'close')}
                 className={tcls(
                     'absolute',
                     'top-3',
@@ -83,9 +85,10 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
                     'justify-center',
                     'items-center',
                     'rounded-sm',
+                    'circular-corners:rounded-full',
                     'hover:bg-tint-hover'
                 )}
-                title={tString(language, 'cookies_close')}
+                title={tString(language, 'close')}
             >
                 <Icon icon="xmark" className={tcls('size-4')} />
             </button>
diff --git a/packages/gitbook/src/components/DocumentView/Block.tsx b/packages/gitbook/src/components/DocumentView/Block.tsx
index 60998b50b1..c9f4ef493a 100644
--- a/packages/gitbook/src/components/DocumentView/Block.tsx
+++ b/packages/gitbook/src/components/DocumentView/Block.tsx
@@ -10,8 +10,10 @@ import {
 } from '@/components/primitives';
 import type { ClassValue } from '@/lib/tailwind';
 
+import { nullIfNever } from '@/lib/typescript';
 import { BlockContentRef } from './BlockContentRef';
 import { CodeBlock } from './CodeBlock';
+import { Columns } from './Columns';
 import { Divider } from './Divider';
 import type { DocumentContextProps } from './DocumentView';
 import { Drawing } from './Drawing';
@@ -44,13 +46,6 @@ export interface BlockProps<Block extends DocumentBlock> extends DocumentContext
     style?: ClassValue;
 }
 
-/**
- * Alternative to `assertNever` that returns `null` instead of throwing an error.
- */
-function nullIfNever(_value: never): null {
-    return null;
-}
-
 export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
     const { block, style, isEstimatedOffscreen, context } = props;
 
@@ -68,6 +63,8 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
                 return <List {...props} block={block} />;
             case 'list-item':
                 return <ListItem {...props} block={block} />;
+            case 'columns':
+                return <Columns {...props} block={block} />;
             case 'code':
                 return <CodeBlock {...props} block={block} />;
             case 'hint':
@@ -112,6 +109,7 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
             case 'image':
             case 'code-line':
             case 'tabs-item':
+            case 'column':
                 throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
             default:
                 return nullIfNever(block);
@@ -168,6 +166,7 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }
         case 'integration':
         case 'stepper':
         case 'reusable-content':
+        case 'columns':
             return <SkeletonCard id={id} style={style} />;
         case 'embed':
         case 'images':
@@ -176,6 +175,7 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }
         case 'image':
         case 'code-line':
         case 'tabs-item':
+        case 'column':
             throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
         default:
             return nullIfNever(block);
diff --git a/packages/gitbook/src/components/DocumentView/Blocks.tsx b/packages/gitbook/src/components/DocumentView/Blocks.tsx
index 903acd2955..4bbe605d87 100644
--- a/packages/gitbook/src/components/DocumentView/Blocks.tsx
+++ b/packages/gitbook/src/components/DocumentView/Blocks.tsx
@@ -70,8 +70,8 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
                 style={[
                     'mx-auto w-full decoration-primary/6',
                     node.data && 'fullWidth' in node.data && node.data.fullWidth
-                        ? 'max-w-screen-xl'
-                        : 'max-w-3xl',
+                        ? 'max-w-screen-2xl'
+                        : 'page-full-width:ml-0 max-w-3xl',
                     blockStyle,
                 ]}
                 isEstimatedOffscreen={isOffscreen}
diff --git a/packages/gitbook/src/components/DocumentView/Caption.tsx b/packages/gitbook/src/components/DocumentView/Caption.tsx
index f7649e68a8..c7075e6bad 100644
--- a/packages/gitbook/src/components/DocumentView/Caption.tsx
+++ b/packages/gitbook/src/components/DocumentView/Caption.tsx
@@ -42,7 +42,7 @@ export function Caption(
             'after:pointer-events-none',
             fit ? 'w-fit' : null,
             withBorder
-                ? 'rounded straight-corners:rounded-none after:border-tint-subtle after:border after:rounded straight-corners:after:rounded-none dark:after:mix-blend-plus-lighter after:pointer-events-none'
+                ? 'rounded circular-corners:rounded-2xl straight-corners:rounded-none after:border-tint-subtle after:border after:rounded circular-corners:after:rounded-2xl straight-corners:after:rounded-none dark:after:mix-blend-plus-lighter after:pointer-events-none'
                 : null,
         ],
         style,
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css
index 6a56801796..71d75354eb 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css
@@ -24,7 +24,7 @@
 }
 
 .highlight-line-number {
-    @apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-gradient-to-r from-80% from-tint to-transparent;
+    @apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-gradient-to-r from-80% from-tint-subtle contrast-more:from-tint-base theme-muted:from-tint-base [html.theme-bold.sidebar-filled_&]:from-tint-base to-transparent;
     @apply before:text-tint before:content-[counter(line)];
 
     .highlight-line.highlighted > & {
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx
index f1f8cb8f63..0ad885a444 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx
@@ -39,7 +39,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
         >
             <div className="flex items-center justify-start gap-2 text-sm [grid-area:1/1]">
                 {title ? (
-                    <div className="inline-flex items-center justify-center rounded-t straight-corners:rounded-t-s bg-tint px-3 py-2 text-tint text-xs leading-none tracking-wide">
+                    <div className="relative top-px z-20 inline-flex items-center justify-center rounded-t straight-corners:rounded-t-s border border-tint-subtle border-b-0 bg-tint-subtle theme-muted:bg-tint-base px-3 py-2 text-tint text-xs leading-none tracking-wide contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base">
                         {title}
                     </div>
                 ) : null}
@@ -50,15 +50,15 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
             />
             <pre
                 className={tcls(
-                    'hide-scroll relative overflow-auto bg-tint theme-gradient:bg-tint-12/1 ring-tint-subtle [grid-area:2/1]',
-                    'rounded-md straight-corners:rounded-sm',
+                    'hide-scroll relative overflow-auto border border-tint-subtle bg-tint-subtle theme-muted:bg-tint-base [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base',
+                    'rounded-md straight-corners:rounded-sm shadow-sm',
                     title && 'rounded-ss-none'
                 )}
             >
                 <code
                     id={id}
                     className={tcls(
-                        'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line]',
+                        'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line] print:whitespace-pre-wrap',
                         withWrap && 'whitespace-pre-wrap'
                     )}
                 >
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts
index eb02b5b928..89d00dafba 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts
@@ -12,6 +12,7 @@ import {
 import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
 import { type BundledLanguage, bundledLanguages } from 'shiki/langs';
 
+import { nullIfNever } from '@/lib/typescript';
 import { plainHighlight } from './plain-highlight';
 
 export type HighlightLine = {
@@ -263,16 +264,28 @@ function getPlainCodeBlockLine(
         if (node.object === 'text') {
             content += cleanupLine(node.leaves.map((leaf) => leaf.text).join(''));
         } else {
-            const start = index + content.length;
-            content += getPlainCodeBlockLine(node, index + content.length, inlines);
-            const end = index + content.length;
-
-            if (inlines) {
-                inlines.push({
-                    inline: node,
-                    start,
-                    end,
-                });
+            switch (node.type) {
+                case 'annotation': {
+                    const start = index + content.length;
+                    content += getPlainCodeBlockLine(node, index + content.length, inlines);
+                    const end = index + content.length;
+
+                    if (inlines) {
+                        inlines.push({
+                            inline: node,
+                            start,
+                            end,
+                        });
+                    }
+                    break;
+                }
+                case 'expression': {
+                    break;
+                }
+                default: {
+                    nullIfNever(node);
+                    break;
+                }
             }
         }
     }
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
index 22ce3b60e3..5e4a73a889 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
@@ -1,31 +1,67 @@
 :root {
     --shiki-color-text: theme("colors.tint.11");
-    --shiki-token-constant: #0a6355;
-    --shiki-token-string: #8b6d32;
-    --shiki-token-comment: theme("colors.teal.700/.64");
-    --shiki-token-keyword: theme("colors.pomegranate.600");
-    --shiki-token-parameter: #0a3069;
-    --shiki-token-function: #8250df;
-    --shiki-token-string-expression: #6a4906;
-    --shiki-token-punctuation: theme("colors.pomegranate.700/.92");
-    --shiki-token-link: theme("colors.tint.12");
-    --shiki-token-inserted: #22863a;
-    --shiki-token-deleted: #b31d28;
-    --shiki-token-changed: #8250df;
+    --shiki-token-punctuation: theme("colors.tint.11");
+    --shiki-token-comment: theme("colors.neutral.9/.7");
+    --shiki-token-link: theme("colors.primary.10");
+
+    --shiki-token-constant: theme("colors.warning.10");
+    --shiki-token-string: theme("colors.warning.10");
+    --shiki-token-string-expression: theme("colors.success.10");
+    --shiki-token-keyword: theme("colors.danger.10");
+    --shiki-token-parameter: theme("colors.warning.10");
+    --shiki-token-function: theme("colors.primary.10");
+
+    --shiki-token-inserted: theme("colors.success.10");
+    --shiki-token-deleted: theme("colors.danger.10");
+    --shiki-token-changed: theme("colors.tint.12");
+}
+
+@media (prefers-contrast: more) {
+    :root {
+        --shiki-color-text: theme("colors.tint.12");
+        --shiki-token-punctuation: theme("colors.tint.12");
+        --shiki-token-comment: theme("colors.neutral.11");
+        --shiki-token-link: theme("colors.primary.11");
+
+        --shiki-token-constant: theme("colors.warning.11");
+        --shiki-token-string: theme("colors.warning.11");
+        --shiki-token-string-expression: theme("colors.success.11");
+        --shiki-token-keyword: theme("colors.danger.11");
+        --shiki-token-parameter: theme("colors.warning.11");
+        --shiki-token-function: theme("colors.primary.11");
+
+        --shiki-token-inserted: theme("colors.success.11");
+        --shiki-token-deleted: theme("colors.danger.11");
+        --shiki-token-changed: theme("colors.tint.12");
+    }
 }
 
 html.dark {
-    --shiki-color-text: theme("colors.tint.11");
-    --shiki-token-constant: #d19a66;
-    --shiki-token-string: theme("colors.pomegranate.300");
-    --shiki-token-comment: theme("colors.teal.300/.64");
-    --shiki-token-keyword: theme("colors.pomegranate.400");
-    --shiki-token-parameter: theme("colors.yellow.500");
-    --shiki-token-function: #56b6c2;
-    --shiki-token-string-expression: theme("colors.tint.11");
-    --shiki-token-punctuation: #acc6ee;
-    --shiki-token-link: theme("colors.pomegranate.400");
-    --shiki-token-inserted: #85e89d;
-    --shiki-token-deleted: #fdaeb7;
-    --shiki-token-changed: #56b6c2;
+    /* Override select colors to have more contrast */
+    --shiki-token-comment: theme("colors.neutral.9");
+
+    --shiki-token-constant: theme("colors.warning.11");
+    --shiki-token-string: theme("colors.warning.11");
+    --shiki-token-string-expression: theme("colors.success.11");
+    --shiki-token-keyword: theme("colors.danger.11");
+    --shiki-token-parameter: theme("colors.warning.11");
+    --shiki-token-function: theme("colors.primary.11");
+}
+
+.code-monochrome {
+    --shiki-token-constant: theme("colors.tint.11");
+    --shiki-token-string: theme("colors.tint.12");
+    --shiki-token-string-expression: theme("colors.tint.12");
+    --shiki-token-keyword: theme("colors.primary.10");
+    --shiki-token-parameter: theme("colors.tint.9");
+    --shiki-token-function: theme("colors.primary.9");
+}
+
+html.dark.code-monochrome {
+    --shiki-token-constant: theme("colors.tint.11");
+    --shiki-token-string: theme("colors.tint.12");
+    --shiki-token-string-expression: theme("colors.tint.12");
+    --shiki-token-keyword: theme("colors.primary.11");
+    --shiki-token-parameter: theme("colors.tint.10");
+    --shiki-token-function: theme("colors.primary.10");
 }
diff --git a/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx b/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx
new file mode 100644
index 0000000000..181095434e
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx
@@ -0,0 +1,80 @@
+import { type ClassValue, tcls } from '@/lib/tailwind';
+import type { DocumentBlockColumns, Length } from '@gitbook/api';
+import type { BlockProps } from '../Block';
+import { Blocks } from '../Blocks';
+
+export function Columns(props: BlockProps<DocumentBlockColumns>) {
+    const { block, style, ancestorBlocks, document, context } = props;
+    return (
+        <div className={tcls('flex flex-col gap-x-8 md:flex-row', style)}>
+            {block.nodes.map((columnBlock) => {
+                const width = columnBlock.data.width;
+                const { className, style } = transformLengthToCSS(width);
+                return (
+                    <Column key={columnBlock.key} className={className} style={style}>
+                        <Blocks
+                            key={columnBlock.key}
+                            nodes={columnBlock.nodes}
+                            document={document}
+                            ancestorBlocks={[...ancestorBlocks, block, columnBlock]}
+                            context={context}
+                            blockStyle="flip-heading-hash"
+                            style="w-full space-y-4 *:max-w-full"
+                        />
+                    </Column>
+                );
+            })}
+        </div>
+    );
+}
+
+export function Column(props: {
+    children?: React.ReactNode;
+    className?: ClassValue;
+    style?: React.CSSProperties;
+}) {
+    return (
+        <div className={tcls('flex-col', props.className)} style={props.style}>
+            {props.children}
+        </div>
+    );
+}
+
+function transformLengthToCSS(length: Length | undefined) {
+    if (!length) {
+        return { className: ['md:w-full'] }; // default to full width if no length is specified
+    }
+
+    if (typeof length === 'number') {
+        return { style: undefined }; // not implemented yet with non-percentage lengths
+    }
+
+    if (length.unit === '%') {
+        return {
+            className: [
+                'md:flex-shrink-0',
+                COLUMN_WIDTHS[Math.round(length.value * 0.01 * (COLUMN_WIDTHS.length - 1))],
+            ],
+        };
+    }
+
+    return { style: undefined }; // not implemented yet with non-percentage lengths
+}
+
+// Tailwind CSS classes for column widths.
+// The index of the array corresponds to the percentage width of the column.
+const COLUMN_WIDTHS = [
+    'md:w-0',
+    'md:w-1/12',
+    'md:w-2/12',
+    'md:w-3/12',
+    'md:w-4/12',
+    'md:w-5/12',
+    'md:w-6/12',
+    'md:w-7/12',
+    'md:w-8/12',
+    'md:w-9/12',
+    'md:w-10/12',
+    'md:w-11/12',
+    'md:w-full',
+];
diff --git a/packages/gitbook/src/components/DocumentView/Columns/index.ts b/packages/gitbook/src/components/DocumentView/Columns/index.ts
new file mode 100644
index 0000000000..a8b4f25b41
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/Columns/index.ts
@@ -0,0 +1 @@
+export * from './Columns';
diff --git a/packages/gitbook/src/components/DocumentView/Divider.tsx b/packages/gitbook/src/components/DocumentView/Divider.tsx
index 8f18593e2f..f787d044b5 100644
--- a/packages/gitbook/src/components/DocumentView/Divider.tsx
+++ b/packages/gitbook/src/components/DocumentView/Divider.tsx
@@ -7,5 +7,5 @@ import type { BlockProps } from './Block';
 export function Divider(props: BlockProps<DocumentBlockDivider>) {
     const { style } = props;
 
-    return <hr className={tcls(style, 'border-tint-subtle')} />;
+    return <hr className={tcls(style, 'page-full-width:max-w-full border-tint-subtle')} />;
 }
diff --git a/packages/gitbook/src/components/DocumentView/DocumentView.tsx b/packages/gitbook/src/components/DocumentView/DocumentView.tsx
index 724a7f2b05..1ff8542eed 100644
--- a/packages/gitbook/src/components/DocumentView/DocumentView.tsx
+++ b/packages/gitbook/src/components/DocumentView/DocumentView.tsx
@@ -28,6 +28,14 @@ export interface DocumentContext {
      * @default true
      */
     wrapBlocksInSuspense?: boolean;
+
+    /**
+     * True if link previews should be rendered.
+     * This is used to limit the number of link previews rendered in a document.
+     * If false, no link previews will be rendered.
+     * @default false
+     */
+    shouldRenderLinkPreviews?: boolean;
 }
 
 export interface DocumentContextProps {
diff --git a/packages/gitbook/src/components/DocumentView/Embed.tsx b/packages/gitbook/src/components/DocumentView/Embed.tsx
index 796cdaeee3..17849c1958 100644
--- a/packages/gitbook/src/components/DocumentView/Embed.tsx
+++ b/packages/gitbook/src/components/DocumentView/Embed.tsx
@@ -6,6 +6,7 @@ import { Card } from '@/components/primitives';
 import { tcls } from '@/lib/tailwind';
 
 import { getDataOrNull } from '@v2/lib/data';
+import { Image } from '../utils';
 import type { BlockProps } from './Block';
 import { Caption } from './Caption';
 import { IntegrationBlock } from './Integration';
@@ -52,7 +53,14 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
                 <Card
                     leadingIcon={
                         embed.icon ? (
-                            <img src={embed.icon} className={tcls('w-5', 'h-5')} alt="Logo" />
+                            <Image
+                                src={embed.icon}
+                                className={tcls('w-5', 'h-5')}
+                                alt="Logo"
+                                sources={{ light: { src: embed.icon } }}
+                                sizes={[{ width: 20 }]}
+                                resize={context.contentContext.imageResizer}
+                            />
                         ) : null
                     }
                     href={block.data.url}
diff --git a/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx b/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx
new file mode 100644
index 0000000000..02f528c459
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx
@@ -0,0 +1,58 @@
+import { type ClassValue, tcls } from '@/lib/tailwind';
+import type { DocumentBlockHeading, DocumentBlockTabs } from '@gitbook/api';
+import { Icon } from '@gitbook/icons';
+import { getBlockTextStyle } from './spacing';
+
+/**
+ * A hash icon which adds the block or active block item's ID in the URL hash.
+ * The button needs to be wrapped in a container with `hashLinkButtonWrapperStyles`.
+ */
+export const hashLinkButtonWrapperStyles = tcls('relative', 'group/hash');
+
+export function HashLinkButton(props: {
+    id: string;
+    block: DocumentBlockTabs | DocumentBlockHeading;
+    label?: string;
+    className?: ClassValue;
+    iconClassName?: ClassValue;
+}) {
+    const { id, block, className, iconClassName, label = 'Direct link to block' } = props;
+    const textStyle = getBlockTextStyle(block);
+    return (
+        <div
+            className={tcls(
+                'relative',
+                'hash',
+                'grid',
+                'grid-area-1-1',
+                'h-[1em]',
+                'border-0',
+                'opacity-0',
+                'group-hover/hash:opacity-[0]',
+                'group-focus/hash:opacity-[0]',
+                'md:group-hover/hash:md:opacity-[1]',
+                'md:group-focus/hash:md:opacity-[1]',
+                className
+            )}
+        >
+            <a
+                href={`#${id}`}
+                aria-label={label}
+                className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
+            >
+                <Icon
+                    icon="hashtag"
+                    className={tcls(
+                        'size-3',
+                        'self-center',
+                        'transition-colors',
+                        'text-transparent',
+                        'group-hover/hash:text-tint-subtle',
+                        'contrast-more:group-hover/hash:text-tint-strong',
+                        iconClassName
+                    )}
+                />
+            </a>
+        </div>
+    );
+}
diff --git a/packages/gitbook/src/components/DocumentView/Heading.tsx b/packages/gitbook/src/components/DocumentView/Heading.tsx
index cb11038321..0de49e623f 100644
--- a/packages/gitbook/src/components/DocumentView/Heading.tsx
+++ b/packages/gitbook/src/components/DocumentView/Heading.tsx
@@ -1,9 +1,9 @@
 import type { DocumentBlockHeading } from '@gitbook/api';
-import { Icon } from '@gitbook/icons';
 
 import { tcls } from '@/lib/tailwind';
 
 import type { BlockProps } from './Block';
+import { HashLinkButton, hashLinkButtonWrapperStyles } from './HashLinkButton';
 import { Inlines } from './Inlines';
 import { getBlockTextStyle } from './spacing';
 
@@ -23,50 +23,20 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
             className={tcls(
                 textStyle.textSize,
                 'heading',
-                'group',
-                'relative',
                 'grid',
                 'scroll-m-12',
+                hashLinkButtonWrapperStyles,
                 style
             )}
         >
-            <div
-                className={tcls(
-                    'hash',
-                    'grid',
-                    'grid-area-1-1',
-                    'relative',
-                    '-ml-6',
-                    'w-7',
-                    'border-0',
-                    'opacity-0',
-                    'group-hover:opacity-[0]',
-                    'group-focus:opacity-[0]',
-                    'md:group-hover:md:opacity-[1]',
-                    'md:group-focus:md:opacity-[1]',
-                    textStyle.marginTop
-                )}
-            >
-                <a
-                    href={`#${id}`}
-                    aria-label="Direct link to heading"
-                    className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
-                >
-                    <Icon
-                        icon="hashtag"
-                        className={tcls(
-                            'w-3.5',
-                            'h-[1em]',
-                            'mt-0.5',
-                            'transition-colors',
-                            'text-transparent',
-                            'group-hover:text-tint-subtle',
-                            'contrast-more:group-hover:text-tint-strong',
-                            'lg:w-4'
-                        )}
-                    />
-                </a>
-            </div>
+            <HashLinkButton
+                id={id}
+                block={block}
+                className={tcls('-ml-6', textStyle.anchorButtonMarginTop)}
+                iconClassName={tcls('size-4')}
+                label="Direct link to heading"
+            />
+
             <div
                 className={tcls(
                     'grid-area-1-1',
diff --git a/packages/gitbook/src/components/DocumentView/Hint.tsx b/packages/gitbook/src/components/DocumentView/Hint.tsx
index 59ff1e0ed7..79961ee172 100644
--- a/packages/gitbook/src/components/DocumentView/Hint.tsx
+++ b/packages/gitbook/src/components/DocumentView/Hint.tsx
@@ -23,6 +23,7 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
                 'rounded-md',
                 hasHeading ? 'rounded-l' : null,
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-xl',
                 'overflow-hidden',
                 hasHeading ? ['border-l-2', hintStyle.containerWithHeader] : hintStyle.container,
 
diff --git a/packages/gitbook/src/components/DocumentView/Images.tsx b/packages/gitbook/src/components/DocumentView/Images.tsx
index baaab50bdc..91d869002e 100644
--- a/packages/gitbook/src/components/DocumentView/Images.tsx
+++ b/packages/gitbook/src/components/DocumentView/Images.tsx
@@ -1,9 +1,4 @@
-import type {
-    DocumentBlockImage,
-    DocumentBlockImageDimension,
-    DocumentBlockImages,
-    JSONDocument,
-} from '@gitbook/api';
+import type { DocumentBlockImage, DocumentBlockImages, JSONDocument, Length } from '@gitbook/api';
 
 import { Image, type ImageResponsiveSize } from '@/components/utils';
 import { resolveContentRef } from '@/lib/references';
@@ -29,7 +24,7 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
                 align === 'center' && 'justify-center',
                 align === 'right' && 'justify-end',
                 align === 'left' && 'justify-start',
-                isMultipleImages && ['grid', 'grid-flow-col', 'max-w-none']
+                isMultipleImages && ['grid', 'grid-flow-col']
             )}
         >
             {block.nodes.map((node: any, _i: number) => (
@@ -119,7 +114,7 @@ async function ImageBlock(props: {
  * When using relative values, the converted dimension will be relative to the parent element's size.
  */
 function getImageDimension<DefaultValue>(
-    dimension: DocumentBlockImageDimension | undefined,
+    dimension: Length | undefined,
     defaultValue: DefaultValue
 ): string | DefaultValue {
     if (typeof dimension === 'number') {
diff --git a/packages/gitbook/src/components/DocumentView/Inline.tsx b/packages/gitbook/src/components/DocumentView/Inline.tsx
index 9101ffef20..84b56decfc 100644
--- a/packages/gitbook/src/components/DocumentView/Inline.tsx
+++ b/packages/gitbook/src/components/DocumentView/Inline.tsx
@@ -1,20 +1,11 @@
-import type {
-    DocumentInline,
-    DocumentInlineAnnotation,
-    DocumentInlineButton,
-    DocumentInlineEmoji,
-    DocumentInlineImage,
-    DocumentInlineLink,
-    DocumentInlineMath,
-    DocumentInlineMention,
-    JSONDocument,
-} from '@gitbook/api';
-import assertNever from 'assert-never';
+import type { DocumentInline, JSONDocument } from '@gitbook/api';
 
+import { nullIfNever } from '@/lib/typescript';
 import { Annotation } from './Annotation/Annotation';
 import type { DocumentContextProps } from './DocumentView';
 import { Emoji } from './Emoji';
 import { InlineButton } from './InlineButton';
+import { InlineIcon } from './InlineIcon';
 import { InlineImage } from './InlineImage';
 import { InlineLink } from './InlineLink';
 import { InlineMath } from './Math';
@@ -39,16 +30,7 @@ export interface InlineProps<T extends DocumentInline> extends DocumentContextPr
     children?: React.ReactNode;
 }
 
-export function Inline<
-    T extends
-        | DocumentInlineImage
-        | DocumentInlineAnnotation
-        | DocumentInlineEmoji
-        | DocumentInlineLink
-        | DocumentInlineMath
-        | DocumentInlineMention
-        | DocumentInlineButton,
->(props: InlineProps<T>) {
+export function Inline<T extends DocumentInline>(props: InlineProps<T>) {
     const { inline, ...contextProps } = props;
 
     switch (inline.type) {
@@ -66,7 +48,13 @@ export function Inline<
             return <InlineImage {...contextProps} inline={inline} />;
         case 'button':
             return <InlineButton {...contextProps} inline={inline} />;
+        case 'icon':
+            return <InlineIcon {...contextProps} inline={inline} />;
+        case 'expression':
+            // The GitBook API should take care of evaluating expressions.
+            // We should never need to render them.
+            return null;
         default:
-            assertNever(inline);
+            return nullIfNever(inline);
     }
 }
diff --git a/packages/gitbook/src/components/DocumentView/InlineButton.tsx b/packages/gitbook/src/components/DocumentView/InlineButton.tsx
index 9841c02626..a36cd74527 100644
--- a/packages/gitbook/src/components/DocumentView/InlineButton.tsx
+++ b/packages/gitbook/src/components/DocumentView/InlineButton.tsx
@@ -17,18 +17,22 @@ export async function InlineButton(props: InlineProps<api.DocumentInlineButton>)
     }
 
     return (
-        <Button
-            href={resolved.href}
-            label={inline.data.label}
-            // TODO: use a variant specifically for user-defined buttons.
-            variant={inline.data.kind}
-            insights={{
-                type: 'link_click',
-                link: {
-                    target: inline.data.ref,
-                    position: api.SiteInsightsLinkPosition.Content,
-                },
-            }}
-        />
+        // Set the leading to have some vertical space between adjacent buttons
+        <span className="inline-button leading-[3rem] [&:has(+.inline-button)]:mr-2">
+            <Button
+                href={resolved.href}
+                label={inline.data.label}
+                // TODO: use a variant specifically for user-defined buttons.
+                variant={inline.data.kind}
+                className="leading-normal"
+                insights={{
+                    type: 'link_click',
+                    link: {
+                        target: inline.data.ref,
+                        position: api.SiteInsightsLinkPosition.Content,
+                    },
+                }}
+            />
+        </span>
     );
 }
diff --git a/packages/gitbook/src/components/DocumentView/InlineIcon.tsx b/packages/gitbook/src/components/DocumentView/InlineIcon.tsx
new file mode 100644
index 0000000000..0eca373f69
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/InlineIcon.tsx
@@ -0,0 +1,10 @@
+import type { DocumentInlineIcon } from '@gitbook/api';
+
+import { Icon, type IconName } from '@gitbook/icons';
+import type { InlineProps } from './Inline';
+
+export async function InlineIcon(props: InlineProps<DocumentInlineIcon>) {
+    const { inline } = props;
+
+    return <Icon icon={inline.data.icon as IconName} className="inline size-[1em]" />;
+}
diff --git a/packages/gitbook/src/components/DocumentView/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink.tsx
deleted file mode 100644
index 37a812bb3c..0000000000
--- a/packages/gitbook/src/components/DocumentView/InlineLink.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
-
-import { resolveContentRef } from '@/lib/references';
-import { Icon } from '@gitbook/icons';
-import { StyledLink } from '../primitives';
-import type { InlineProps } from './Inline';
-import { InlineLinkTooltip } from './InlineLinkTooltip';
-import { Inlines } from './Inlines';
-
-export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
-    const { inline, document, context, ancestorInlines } = props;
-
-    const resolved = context.contentContext
-        ? await resolveContentRef(inline.data.ref, context.contentContext, {
-              resolveAnchorText: true,
-          })
-        : null;
-
-    if (!context.contentContext || !resolved) {
-        return (
-            <span title="Broken link" className="underline">
-                <Inlines
-                    context={context}
-                    document={document}
-                    nodes={inline.nodes}
-                    ancestorInlines={[...ancestorInlines, inline]}
-                />
-            </span>
-        );
-    }
-    const isExternal = inline.data.ref.kind === 'url';
-
-    return (
-        <InlineLinkTooltip inline={inline} context={context.contentContext} resolved={resolved}>
-            <StyledLink
-                href={resolved.href}
-                insights={{
-                    type: 'link_click',
-                    link: {
-                        target: inline.data.ref,
-                        position: SiteInsightsLinkPosition.Content,
-                    },
-                }}
-            >
-                <Inlines
-                    context={context}
-                    document={document}
-                    nodes={inline.nodes}
-                    ancestorInlines={[...ancestorInlines, inline]}
-                />
-                {isExternal ? (
-                    <Icon
-                        icon="arrow-up-right"
-                        className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
-                    />
-                ) : null}
-            </StyledLink>
-        </InlineLinkTooltip>
-    );
-}
diff --git a/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx
new file mode 100644
index 0000000000..8ad287a351
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx
@@ -0,0 +1,153 @@
+import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
+
+import { getSpaceLanguage, tString } from '@/intl/server';
+import { languages } from '@/intl/translations';
+import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
+import { Icon } from '@gitbook/icons';
+import type { GitBookAnyContext } from '@v2/lib/context';
+import { StyledLink } from '../../primitives';
+import type { InlineProps } from '../Inline';
+import { Inlines } from '../Inlines';
+import { InlineLinkTooltip } from './InlineLinkTooltip';
+
+export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
+    const { inline, document, context, ancestorInlines } = props;
+
+    const resolved = context.contentContext
+        ? await resolveContentRef(inline.data.ref, context.contentContext, {
+              // We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link.
+              resolveAnchorText: false,
+          })
+        : null;
+
+    if (!context.contentContext || !resolved) {
+        return (
+            <span title="Broken link" className="underline">
+                <Inlines
+                    context={context}
+                    document={document}
+                    nodes={inline.nodes}
+                    ancestorInlines={[...ancestorInlines, inline]}
+                />
+            </span>
+        );
+    }
+    const isExternal = inline.data.ref.kind === 'url';
+    const content = (
+        <StyledLink
+            href={resolved.href}
+            insights={{
+                type: 'link_click',
+                link: {
+                    target: inline.data.ref,
+                    position: SiteInsightsLinkPosition.Content,
+                },
+            }}
+        >
+            <Inlines
+                context={context}
+                document={document}
+                nodes={inline.nodes}
+                ancestorInlines={[...ancestorInlines, inline]}
+            />
+            {isExternal ? (
+                <Icon
+                    icon="arrow-up-right"
+                    className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
+                />
+            ) : null}
+        </StyledLink>
+    );
+
+    if (context.shouldRenderLinkPreviews) {
+        return (
+            <InlineLinkTooltipWrapper
+                inline={inline}
+                context={context.contentContext}
+                resolved={resolved}
+            >
+                {content}
+            </InlineLinkTooltipWrapper>
+        );
+    }
+
+    return content;
+}
+
+/**
+ * An SSR component that renders a link with a tooltip.
+ * Essentially it pulls the minimum amount of props from the context to render the tooltip.
+ */
+function InlineLinkTooltipWrapper(props: {
+    inline: DocumentInlineLink;
+    context: GitBookAnyContext;
+    children: React.ReactNode;
+    resolved: ResolvedContentRef;
+}) {
+    const { inline, context, resolved, children } = props;
+
+    let breadcrumbs = resolved.ancestors ?? [];
+    const language =
+        'customization' in context ? getSpaceLanguage(context.customization) : languages.en;
+    const isExternal = inline.data.ref.kind === 'url';
+    const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
+    if (isExternal) {
+        breadcrumbs = [
+            {
+                label: tString(language, 'link_tooltip_external_link'),
+            },
+        ];
+    }
+    if (isSamePage) {
+        breadcrumbs = [
+            {
+                label: tString(language, 'link_tooltip_page_anchor'),
+                icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
+            },
+        ];
+        resolved.subText = undefined;
+    }
+
+    const aiSummary: { pageId: string; spaceId: string } | undefined = (() => {
+        if (isExternal) {
+            return;
+        }
+
+        if (isSamePage) {
+            return;
+        }
+
+        if (!('customization' in context) || !context.customization.ai?.pageLinkSummaries.enabled) {
+            return;
+        }
+
+        if (!('page' in context) || !('page' in inline.data.ref)) {
+            return;
+        }
+
+        if (inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor') {
+            return {
+                pageId: resolved.page?.id ?? inline.data.ref.page ?? context.page.id,
+                spaceId: inline.data.ref.space ?? context.space.id,
+            };
+        }
+    })();
+
+    return (
+        <InlineLinkTooltip
+            breadcrumbs={breadcrumbs}
+            isExternal={isExternal}
+            isSamePage={isSamePage}
+            aiSummary={aiSummary}
+            openInNewTabLabel={tString(language, 'open_in_new_tab')}
+            target={{
+                href: resolved.href,
+                text: resolved.text,
+                subText: resolved.subText,
+                icon: resolved.icon,
+            }}
+        >
+            {children}
+        </InlineLinkTooltip>
+    );
+}
diff --git a/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltip.tsx b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltip.tsx
new file mode 100644
index 0000000000..45be0c7173
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltip.tsx
@@ -0,0 +1,73 @@
+'use client';
+import dynamic from 'next/dynamic';
+import React from 'react';
+
+const LoadingValueContext = React.createContext<React.ReactNode>(null);
+
+// To avoid polluting the RSC payload with the tooltip implementation,
+// we lazily load it on the client side. This way, the tooltip is only loaded
+// when the user interacts with the link, and it doesn't block the initial render.
+
+const InlineLinkTooltipImpl = dynamic(
+    () => import('./InlineLinkTooltipImpl').then((mod) => mod.InlineLinkTooltipImpl),
+    {
+        // Disable server-side rendering for this component, it's only
+        // visible on user interaction.
+        ssr: false,
+        loading: () => {
+            // The fallback should be the children (the content of the link),
+            // but as next/dynamic is aiming for feature parity with React.lazy,
+            // it doesn't support passing children to the loading component.
+            // https://github.com/vercel/next.js/issues/7906
+            const children = React.useContext(LoadingValueContext);
+            return <>{children}</>;
+        },
+    }
+);
+
+/**
+ * Tooltip for inline links. It's lazily loaded to avoid blocking the initial render
+ * and polluting the RSC payload.
+ *
+ * The link text and href have already been rendered on the server for good SEO,
+ * so we can be as lazy as possible with the tooltip.
+ */
+export function InlineLinkTooltip(props: {
+    isSamePage: boolean;
+    isExternal: boolean;
+    aiSummary?: { pageId: string; spaceId: string };
+    breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>;
+    target: {
+        href: string;
+        text: string;
+        subText?: string;
+        icon?: React.ReactNode;
+    };
+    openInNewTabLabel: string;
+    children: React.ReactNode;
+}) {
+    const { children, ...rest } = props;
+    const [shouldLoad, setShouldLoad] = React.useState(false);
+
+    // Once the browser is idle, we set shouldLoad to true.
+    // NOTE: to be slightly more performant, we could load when a link is hovered.
+    // But I found this was too much of a delay for the tooltip to appear.
+    // Loading on idle is a good compromise, as it allows the initial render to be fast,
+    // while still loading the tooltip in the background and not polluting the RSC payload.
+    React.useEffect(() => {
+        if ('requestIdleCallback' in window) {
+            (window as globalThis.Window).requestIdleCallback(() => setShouldLoad(true));
+        } else {
+            // fallback for old browsers
+            setTimeout(() => setShouldLoad(true), 2000);
+        }
+    }, []);
+
+    return shouldLoad ? (
+        <LoadingValueContext.Provider value={children}>
+            <InlineLinkTooltipImpl {...rest}>{children}</InlineLinkTooltipImpl>
+        </LoadingValueContext.Provider>
+    ) : (
+        children
+    );
+}
diff --git a/packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltipImpl.tsx
similarity index 64%
rename from packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx
rename to packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltipImpl.tsx
index 1a5cefa1e3..333184a2cd 100644
--- a/packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx
+++ b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltipImpl.tsx
@@ -1,55 +1,27 @@
-import type { DocumentInlineLink } from '@gitbook/api';
-
-import type { ResolvedContentRef } from '@/lib/references';
-
-import { getSpaceLanguage } from '@/intl/server';
-import { tString } from '@/intl/translate';
-import { languages } from '@/intl/translations';
-import { getNodeText } from '@/lib/document';
+'use client';
 import { tcls } from '@/lib/tailwind';
 import { Icon } from '@gitbook/icons';
 import * as Tooltip from '@radix-ui/react-tooltip';
-import type { GitBookAnyContext } from '@v2/lib/context';
 import { Fragment } from 'react';
-import { AIPageLinkSummary } from '../Adaptive/AIPageLinkSummary';
-import { Button, StyledLink } from '../primitives';
+import { AIPageLinkSummary } from '../../Adaptive';
+import { Button, StyledLink } from '../../primitives';
 
-export async function InlineLinkTooltip(props: {
-    inline: DocumentInlineLink;
-    context: GitBookAnyContext;
+export function InlineLinkTooltipImpl(props: {
+    isSamePage: boolean;
+    isExternal: boolean;
+    aiSummary?: { pageId: string; spaceId: string };
+    breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>;
+    target: {
+        href: string;
+        text: string;
+        subText?: string;
+        icon?: React.ReactNode;
+    };
+    openInNewTabLabel: string;
     children: React.ReactNode;
-    resolved: ResolvedContentRef;
 }) {
-    const { inline, context, resolved, children } = props;
-
-    let breadcrumbs = resolved.ancestors;
-    const language =
-        'customization' in context ? getSpaceLanguage(context.customization) : languages.en;
-    const isExternal = inline.data.ref.kind === 'url';
-    const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
-    if (isExternal) {
-        breadcrumbs = [
-            {
-                label: tString(language, 'link_tooltip_external_link'),
-            },
-        ];
-    }
-    if (isSamePage) {
-        breadcrumbs = [
-            {
-                label: tString(language, 'link_tooltip_page_anchor'),
-                icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
-            },
-        ];
-        resolved.subText = undefined;
-    }
-
-    const hasAISummary =
-        !isExternal &&
-        !isSamePage &&
-        'customization' in context &&
-        context.customization.ai?.pageLinkSummaries.enabled &&
-        (inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor');
+    const { isSamePage, isExternal, aiSummary, openInNewTabLabel, target, breadcrumbs, children } =
+        props;
 
     return (
         <Tooltip.Provider delayDuration={200}>
@@ -100,15 +72,15 @@ export async function InlineLinkTooltip(props: {
                                                 isExternal && 'text-sm [overflow-wrap:anywhere]'
                                             )}
                                         >
-                                            {resolved.icon ? (
+                                            {target.icon ? (
                                                 <div className="mt-1 text-tint-subtle empty:hidden">
-                                                    {resolved.icon}
+                                                    {target.icon}
                                                 </div>
                                             ) : null}
-                                            <h5 className="font-semibold">{resolved.text}</h5>
+                                            <h5 className="font-semibold">{target.text}</h5>
                                         </div>
                                     </div>
-                                    {!isSamePage && resolved.href ? (
+                                    {!isSamePage && target.href ? (
                                         <Button
                                             className={tcls(
                                                 '-mx-2 -my-2 ml-auto',
@@ -117,40 +89,35 @@ export async function InlineLinkTooltip(props: {
                                                     : null
                                             )}
                                             variant="blank"
-                                            href={resolved.href}
+                                            href={target.href}
                                             target="_blank"
-                                            label={tString(language, 'open_in_new_tab')}
+                                            label={openInNewTabLabel}
                                             size="small"
                                             icon="arrow-up-right-from-square"
                                             iconOnly={true}
                                         />
                                     ) : null}
                                 </div>
-                                {resolved.subText ? (
-                                    <p className="mt-1 text-sm text-tint">{resolved.subText}</p>
+                                {target.subText ? (
+                                    <p className="mt-1 text-sm text-tint">{target.subText}</p>
                                 ) : null}
                             </div>
 
-                            {hasAISummary && 'page' in context && 'page' in inline.data.ref ? (
+                            {aiSummary ? (
                                 <div className="border-tint-subtle border-t bg-tint p-4">
                                     <AIPageLinkSummary
-                                        targetPageId={
-                                            resolved.page?.id ??
-                                            inline.data.ref.page ??
-                                            context.page.id
-                                        }
-                                        targetSpaceId={inline.data.ref.space ?? context.space.id}
-                                        linkTitle={getNodeText(inline)}
-                                        linkPreview={`**${resolved.text}**: ${resolved.subText}`}
-                                        showTrademark={
-                                            'customization' in context &&
-                                            context.customization.trademark.enabled
-                                        }
+                                        targetPageId={aiSummary.pageId}
+                                        targetSpaceId={aiSummary.spaceId}
+                                        showTrademark
                                     />
                                 </div>
                             ) : null}
                         </div>
-                        <Tooltip.Arrow className={hasAISummary ? 'fill-tint-3' : 'fill-tint-1'} />
+                        <Tooltip.Arrow
+                            className={
+                                typeof aiSummary !== 'undefined' ? 'fill-tint-3' : 'fill-tint-1'
+                            }
+                        />
                     </Tooltip.Content>
                 </Tooltip.Portal>
             </Tooltip.Root>
diff --git a/packages/gitbook/src/components/DocumentView/InlineLink/index.ts b/packages/gitbook/src/components/DocumentView/InlineLink/index.ts
new file mode 100644
index 0000000000..1d7f30120b
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/InlineLink/index.ts
@@ -0,0 +1 @@
+export * from './InlineLink';
diff --git a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx
index 6ebec6f030..a26aa3bfd2 100644
--- a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx
+++ b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx
@@ -5,8 +5,8 @@ import { GITBOOK_INTEGRATIONS_HOST } from '@v2/lib/env';
 
 import type { BlockProps } from '../Block';
 import './contentkit.css';
-import { getDataOrNull } from '@v2/lib/data';
 import { contentKitServerContext } from './contentkit';
+import { fetchSafeIntegrationUI } from './render';
 import { renderIntegrationUi } from './server-actions';
 
 export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegration>) {
@@ -16,8 +16,6 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
         throw new Error('integration block requires a content.spaceId');
     }
 
-    const { dataFetcher } = context.contentContext;
-
     const initialInput: RenderIntegrationUI = {
         componentId: block.data.block,
         props: block.data.props,
@@ -30,17 +28,27 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
         },
     };
 
-    const initialOutput = await getDataOrNull(
-        dataFetcher.renderIntegrationUi({
-            integrationName: block.data.integration,
-            request: initialInput,
-        }),
+    const initialResponse = await fetchSafeIntegrationUI(context.contentContext, {
+        integrationName: block.data.integration,
+        request: initialInput,
+    });
 
-        // The API can respond with a 400 error if the integration is not installed
-        // and 404 if the integration is not found.
-        [404, 400]
-    );
-    if (!initialOutput || initialOutput.type === 'complete') {
+    if (initialResponse.error) {
+        if (initialResponse.error.code === 404) {
+            return null;
+        }
+
+        return (
+            <div className={tcls(style)}>
+                <pre>
+                    Unexpected error with integration {block.data.integration}:{' '}
+                    {initialResponse.error.message}
+                </pre>
+            </div>
+        );
+    }
+    const initialOutput = initialResponse.data;
+    if (initialOutput.type === 'complete') {
         return null;
     }
 
diff --git a/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx b/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx
index fb692c6824..155077ca25 100644
--- a/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx
+++ b/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx
@@ -20,6 +20,8 @@ export const contentKitServerContext: ContentKitServerContext = {
         'link-external': (props) => <Icon icon="arrow-up-right-from-square" {...props} />,
         eye: (props) => <Icon icon="eye" {...props} />,
         lock: (props) => <Icon icon="lock" {...props} />,
+        check: (props) => <Icon icon="check" {...props} />,
+        'check-circle': (props) => <Icon icon="check-circle" {...props} />,
     },
     codeBlock: (props) => {
         return <PlainCodeBlock code={props.code} syntax={props.syntax} />;
diff --git a/packages/gitbook/src/components/DocumentView/Integration/render.ts b/packages/gitbook/src/components/DocumentView/Integration/render.ts
new file mode 100644
index 0000000000..4e683e0dcd
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/Integration/render.ts
@@ -0,0 +1,34 @@
+import type { RenderIntegrationUI } from '@gitbook/api';
+import type { GitBookBaseContext } from '@v2/lib/context';
+import { ignoreDataFetcherErrors } from '@v2/lib/data';
+
+/**
+ * Render an integration UI while ignoring some errors.
+ */
+export async function fetchSafeIntegrationUI(
+    context: GitBookBaseContext,
+    {
+        integrationName,
+        request,
+    }: {
+        integrationName: string;
+        request: RenderIntegrationUI;
+    }
+) {
+    const output = await ignoreDataFetcherErrors(
+        context.dataFetcher.renderIntegrationUi({
+            integrationName,
+            request,
+        }),
+
+        // The API can respond with certain errors that are expected to happen.
+        [
+            404, // Integration has been uninstalled
+            400, // Integration is rejecting its own request
+            422, // Integration is triggering an invalid request, failing at the validation step
+            502, // Integration is failing in an unexpected way
+        ]
+    );
+
+    return output;
+}
diff --git a/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx b/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx
index 54180e5c89..91d1e0e73c 100644
--- a/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx
+++ b/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx
@@ -4,9 +4,9 @@ import { getV1BaseContext } from '@/lib/v1';
 import { isV2 } from '@/lib/v2';
 import type { RenderIntegrationUI } from '@gitbook/api';
 import { ContentKitOutput } from '@gitbook/react-contentkit';
-import { throwIfDataError } from '@v2/lib/data';
 import { getServerActionBaseContext } from '@v2/lib/server-actions';
 import { contentKitServerContext } from './contentkit';
+import { fetchSafeIntegrationUI } from './render';
 
 /**
  * Server action to render an integration UI request from <ContentKit />.
@@ -22,16 +22,19 @@ export async function renderIntegrationUi({
     request: RenderIntegrationUI;
 }) {
     const serverAction = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
+    const output = await fetchSafeIntegrationUI(serverAction, {
+        integrationName: renderContext.integrationName,
+        request,
+    });
 
-    const output = await throwIfDataError(
-        serverAction.dataFetcher.renderIntegrationUi({
-            integrationName: renderContext.integrationName,
-            request,
-        })
-    );
+    if (output.error) {
+        return {
+            error: output.error.message,
+        };
+    }
 
     return {
-        children: <ContentKitOutput output={output} context={contentKitServerContext} />,
-        output: output,
+        children: <ContentKitOutput output={output.data} context={contentKitServerContext} />,
+        output: output.data,
     };
 }
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css b/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css
index 8096a9fbef..b7eec226f9 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css
@@ -271,7 +271,7 @@ body {
 }
 .scalar-activate-button {
     @apply flex gap-2 items-center;
-    @apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none place-self-start;
+    @apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none circular-corners:rounded-full circular-corners:px-3 place-self-start;
     @apply ring-1 ring-tint hover:ring-tint-hover;
     @apply shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none;
     @apply contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12;
diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index 39cccc4b38..337f0b2ed2 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -6,7 +6,7 @@
 }
 
 .openapi-schemas {
-    @apply flex flex-col mb-14 flex-1;
+    @apply flex flex-col mb-14 gap-0 flex-1;
 }
 
 .openapi-schemas-title {
@@ -145,7 +145,7 @@
 }
 
 .openapi-column-preview-body {
-    @apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header-sections:top-32 page-api-block:xl:max-2xl:top-32 print-mode:static;
+    @apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header:xl:max-2xl:top-32 site-header-sections:top-32 site-header-sections:xl:max-2xl:top-44 print-mode:static;
 }
 
 .openapi-column-preview pre {
@@ -169,41 +169,18 @@
     @apply flex flex-col;
 }
 
-.openapi-schema {
+.openapi-schema,
+.openapi-disclosure {
     @apply py-2.5 flex flex-col gap-2;
 }
 
-.openapi-section-body .openapi-schema-properties {
-    @apply divide-y divide-tint-subtle;
-}
-
-.openapi-disclosure-group-panel > .openapi-schema-properties > *:first-child > .openapi-schema {
-    @apply pt-0;
-}
-
-.openapi-responsebody > .openapi-schema-properties > .openapi-schema:last-child {
-    @apply pb-0;
-}
-
-.openapi-responsebody > .openapi-schema-properties > .openapi-schema:only-child {
-    @apply py-0;
-}
-
 .openapi-schema-properties .openapi-schema:last-child {
     @apply border-b-0;
 }
 
-.openapi-schema-properties .openapi-schema-opened {
-    @apply pb-3;
-}
-
-.openapi-schema > .openapi-schema-properties {
-    @apply mt-3;
-}
-
 /* Schema Presentation */
 .openapi-schema-presentation {
-    @apply flex flex-col gap-1.5 font-normal;
+    @apply flex flex-col gap-1 font-normal;
 }
 
 .openapi-schema-properties:last-child {
@@ -221,7 +198,7 @@
 }
 
 .openapi-schema-propertyname {
-    @apply select-all font-mono font-normal text-tint-strong;
+    @apply select-all font-mono font-semibold text-tint-strong;
 }
 
 .openapi-schema-propertyname[data-deprecated="true"] {
@@ -233,7 +210,7 @@
 }
 
 .openapi-schema-optional {
-    @apply text-info-subtle text-[0.813rem] lowercase;
+    @apply text-tint-subtle text-[0.813rem] lowercase;
 }
 
 .openapi-schema-readonly {
@@ -244,6 +221,10 @@
     @apply text-success dark:text-success-subtle/9 text-[0.813rem] lowercase;
 }
 
+.openapi-schema-types {
+    @apply flex items-baseline flex-wrap gap-1;
+}
+
 .openapi-schema-type {
     @apply text-tint select-text text-[0.813rem] font-mono [word-spacing:-0.25rem];
 }
@@ -321,7 +302,7 @@
 .openapi-schema-pattern code,
 .openapi-schema-enum-value code,
 .openapi-schema-default code {
-    @apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
+    @apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint-subtle bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
 }
 
 /* Authentication */
@@ -329,6 +310,26 @@
     @apply py-2 border-b border-tint-subtle max-w-full flex-1;
 }
 
+.openapi-securities-oauth-flows {
+    @apply flex flex-col gap-2 divide-y divide-tint-subtle;
+}
+
+.openapi-securities-oauth-content {
+    @apply prose *:!prose-sm *:text-tint;
+}
+
+.openapi-securities-oauth-content.openapi-markdown code {
+    @apply text-xs;
+}
+
+.openapi-securities-oauth-content ul {
+    @apply !my-0;
+}
+
+.openapi-securities-url {
+    @apply ml-0.5 px-0.5 rounded hover:bg-tint transition-colors;
+}
+
 .openapi-securities-body {
     @apply flex flex-col gap-2;
 }
@@ -373,28 +374,25 @@
 }
 
 .openapi-response-tab-content {
-    @apply overflow-hidden max-w-full flex items-baseline;
-    @apply text-left text-pretty relative leading-[1.125rem] text-tint !font-normal truncate select-text;
+    @apply flex items-baseline truncate grow shrink max-w-max basis-[60%] mr-auto;
+    @apply text-left text-pretty relative leading-tight text-tint select-text;
 }
 
 .openapi-response-description.openapi-markdown {
-    @apply text-left prose-sm text-[0.813rem] text-pretty h-auto relative leading-[1.125rem] text-tint !font-normal truncate select-text prose-strong:font-semibold prose-strong:text-inherit;
-}
-
-.openapi-response-description.openapi-markdown::-webkit-scrollbar {
-    display: none;
+    @apply text-left truncate prose-sm text-sm leading-tight text-tint select-text prose-strong:font-semibold prose-strong:text-inherit;
 }
 
-.openapi-response-description p {
-    @apply truncate max-w-full inline pr-1;
+.openapi-disclosure-group-trigger[aria-expanded="true"] .openapi-response-tab-content {
+    @apply basis-full;
 }
 
-.openapi-response-content-type {
-    @apply text-xs text-tint-8 ml-auto shrink-0;
+.openapi-disclosure-group-trigger[aria-expanded="true"]
+    .openapi-response-description.openapi-markdown {
+    @apply whitespace-normal;
 }
 
 .openapi-response-body {
-    @apply flex flex-col gap-3;
+    @apply flex flex-col;
 }
 
 /* Response Body and Headers */
@@ -411,16 +409,7 @@
     @apply px-3 py-1;
 }
 
-.openapi-responsebody-header-content,
-.openapi-responseheaders-header-content {
-    /* unstyled */
-}
-
 /* Code Sample */
-.openapi-codesample {
-    @apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle;
-}
-
 .openapi-codesample-header {
     @apply flex flex-row items-center;
 }
@@ -429,6 +418,12 @@
     @apply flex flex-row items-center gap-2.5;
 }
 
+.openapi-panel-heading,
+.openapi-codesample-header,
+.openapi-response-examples-header {
+    @apply border-b border-tint-subtle;
+}
+
 .openapi-response-examples-header .openapi-select > button {
     @apply max-w-full overflow-hidden shrink pl-0.5 py-0.5;
 }
@@ -503,8 +498,16 @@
 }
 
 /* Panel */
-.openapi-panel {
-    @apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle;
+.openapi-panel,
+.openapi-codesample,
+.openapi-response-examples {
+    @apply border rounded-md straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
+}
+
+.openapi-panel pre,
+.openapi-codesample pre,
+.openapi-response-examples pre {
+    @apply bg-transparent border-none rounded-none shadow-none;
 }
 
 .openapi-panel-heading {
@@ -513,12 +516,11 @@
 
 .openapi-panel-body {
     @apply relative;
-    @apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
 }
 
 .openapi-panel-footer,
 .openapi-codesample-footer {
-    @apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
+    @apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint empty:hidden;
 }
 
 .openapi-panel-footer .openapi-markdown {
@@ -526,10 +528,6 @@
 }
 
 /* Example */
-.openapi-response-examples {
-    @apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle;
-}
-
 .openapi-response-examples-header {
     @apply flex flex-row items-center p-2.5;
 }
@@ -551,7 +549,6 @@
 .openapi-response-examples-panel,
 .openapi-codesample-panel {
     @apply flex-1 text-sm relative focus-visible:outline-none;
-    @apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
 }
 
 .openapi-example-empty {
@@ -594,7 +591,7 @@ body:has(.openapi-select-popover) {
 }
 
 .openapi-select-popover {
-    @apply min-w-32 z-10 max-w-[max(20rem,var(--trigger-width))] overflow-x-hidden max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md straight-corners:rounded-none;
+    @apply min-w-32 z-10 max-w-[max(20rem,var(--trigger-width))] overflow-x-hidden max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md circular-corners:rounded-xl straight-corners:rounded-none;
     @apply shadow-md shadow-tint-12/1 dark:shadow-tint-1/1;
 }
 
@@ -703,27 +700,31 @@ body:has(.openapi-select-popover) {
 
 /* Disclosure group */
 .openapi-disclosure-group {
-    @apply border-tint-subtle border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none first:rounded-t-md straight-corners:first:rounded-none first:border-t relative;
+    @apply border-tint-subtle transition-all border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none circular-corners:last:rounded-b-xl first:rounded-t-md straight-corners:first:rounded-none circular-corners:first:rounded-t-xl first:border-t relative;
+}
+
+.openapi-disclosure-group:has(.openapi-disclosure-group-trigger:hover) {
+    @apply bg-tint-subtle;
 }
 
-.openapi-disclosure-group-header {
-    @apply flex flex-row items-baseline justify-between gap-3 relative;
+.openapi-disclosure-group:has(.openapi-disclosure-group-trigger:hover):has(.openapi-select:hover) {
+    @apply !bg-transparent;
 }
 
 .openapi-disclosure-group-trigger {
-    @apply flex items-baseline transition-all hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1;
+    @apply flex w-full cursor-pointer items-baseline gap-3 transition-all relative flex-1 p-3 -outline-offset-1;
 }
 
-.openapi-disclosure-group-trigger:disabled {
-    @apply cursor-default hover:bg-inherit;
+.openapi-disclosure-group-label {
+    @apply flex flex-wrap items-baseline gap-x-3 gap-y-1 flex-1 truncate;
 }
 
-.openapi-disclosure-group-trigger:disabled .openapi-disclosure-group-icon {
-    @apply invisible;
+.openapi-disclosure-group-trigger[aria-disabled="true"] {
+    @apply cursor-default hover:bg-inherit;
 }
 
-.openapi-disclosure-group-trigger[aria-expanded="true"] .openapi-response-description {
-    @apply whitespace-normal;
+.openapi-disclosure-group-trigger[aria-disabled="true"] .openapi-disclosure-group-icon {
+    @apply invisible;
 }
 
 .openapi-disclosure-group-icon > svg {
@@ -735,29 +736,24 @@ body:has(.openapi-select-popover) {
 }
 
 .openapi-disclosure-group-panel {
-    @apply p-3 pt-1 transition-all;
+    @apply px-3 transition-all;
 }
 
 .openapi-disclosure-group-trigger[aria-expanded="true"] > .openapi-disclosure-group-icon > svg {
     @apply rotate-90;
 }
 
-.openapi-disclosure-group:hover .openapi-disclosure-group-mediatype,
-.openapi-disclosure-group-mediatype:has(> .openapi-select[data-open="true"]) {
-    @apply opacity-11 visible flex;
-}
-
-.openapi-disclosure-group-mediatype {
-    @apply opacity-0 invisible text-xs transition-opacity duration-300 shrink-0 absolute right-2.5 top-2.5;
+.openapi-disclosure-group-mediatype:not(:has(.openapi-select)) {
+    @apply text-[0.625rem] font-mono shrink-0 grow-0 text-tint-subtle contrast-more:text-tint;
 }
 
-.openapi-disclosure-group-mediatype > span {
-    @apply px-1 bg-tint-6 text-tint-12 rounded-full straight-corners:rounded-md;
+/* Disclosure */
+.openapi-schemas-disclosure {
+    @apply border-t border-x last:border-b border-tint-subtle !ring-0 first:!rounded-t-xl last:!rounded-b-xl !rounded-none;
 }
 
-/* Disclosure */
 .openapi-schemas-disclosure > .openapi-disclosure-trigger {
-    @apply flex items-center font-mono !w-full transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1;
+    @apply flex items-center font-mono transition-all font-normal text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-5 truncate -outline-offset-1;
 }
 
 .openapi-schemas-disclosure > .openapi-disclosure-trigger,
@@ -765,49 +761,94 @@ body:has(.openapi-select-popover) {
     @apply straight-corners:!rounded-none;
 }
 
+.openapi-disclosure-panel {
+    @apply ml-1.5 pl-3 border-l border-tint-subtle;
+}
+
+.openapi-schema .openapi-schema-properties .openapi-schema {
+    @apply animate-fadeIn [animation-fill-mode:both];
+}
+
 .openapi-schemas-disclosure > .openapi-disclosure-trigger[aria-expanded="true"] > svg {
     @apply rotate-90;
 }
 
 .openapi-disclosure-trigger {
-    @apply transition-all truncate duration-300 max-w-full hover:text-tint-strong rounded-2xl straight-corners:rounded border border-tint-subtle px-2.5 py-1 text-[0.813rem] text-tint flex flex-row items-center gap-1.5 -outline-offset-1;
+    @apply flex flex-row justify-between flex-wrap relative items-start gap-2 text-left -mx-3 px-3 -my-2.5 py-2.5 pr-10;
 }
 
-.openapi-disclosure-trigger span {
-    @apply truncate;
+.openapi-disclosure {
+    @apply -mx-3 px-3 py-2.5 transition-all flex flex-col ring-tint-subtle;
 }
 
-.openapi-disclosure svg {
-    @apply size-3 shrink-0 transition-transform duration-300;
+.openapi-disclosure:not(
+        .openapi-disclosure-group .openapi-disclosure,
+        .openapi-schema-alternatives .openapi-disclosure,
+        .openapi-schemas-disclosure .openapi-schema.openapi-disclosure
+    ) {
+    @apply rounded-xl;
 }
 
-.openapi-disclosure-trigger[aria-expanded="true"] svg {
-    @apply rotate-45;
+.openapi-disclosure .openapi-schemas-disclosure .openapi-schema.openapi-disclosure {
+    @apply !rounded-none;
 }
 
-.openapi-disclosure-trigger[aria-expanded="true"] {
-    @apply w-full rounded-lg border-b rounded-b-none straight-corners:rounded-b-none;
+.openapi-disclosure:has(> .openapi-disclosure-trigger:hover) {
+    @apply bg-tint-subtle overflow-hidden;
 }
 
-.openapi-disclosure-trigger[aria-expanded="false"] {
-    @apply w-auto;
+.openapi-disclosure[data-expanded="true"] {
+    @apply ring-1 shadow-sm;
 }
 
-.openapi-disclosure-panel[aria-hidden="false"] {
-    @apply border-b border-x border-tint-subtle rounded-b-lg straight-corners:rounded-b;
+.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:first-child) {
+    @apply mt-2;
+}
+.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:last-child) {
+    @apply mb-2;
 }
 
-.openapi-disclosure-panel .openapi-schema {
-    @apply p-2.5;
+.openapi-disclosure-trigger-label {
+    @apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs;
 }
 
-.openapi-disclosure .openapi-schema-properties .openapi-schema:only-child,
-.openapi-disclosure .openapi-schema-properties .openapi-schema:only-child .openapi-schema-name {
-    @apply !m-0;
+.openapi-disclosure-trigger-label span {
+    @apply hidden;
 }
 
-.openapi-disclosure .openapi-schema-properties .openapi-schema-enum {
-    @apply pt-0 mt-0;
+.openapi-disclosure-trigger-label svg {
+    @apply size-3 shrink-0 transition-transform duration-300 text-tint-subtle;
+}
+
+.openapi-disclosure-trigger:hover > .openapi-disclosure-trigger-label,
+.openapi-disclosure-trigger[aria-expanded="true"] > .openapi-disclosure-trigger-label {
+    @apply shadow ring-1 bg-tint-base;
+}
+
+.openapi-disclosure-trigger:hover > .openapi-disclosure-trigger-label span,
+.openapi-disclosure-trigger[aria-expanded="true"] > .openapi-disclosure-trigger-label span {
+    @apply block animate-fadeIn;
+}
+
+@media (hover: none) {
+    /* Make button label always visible on non-hover devices like phones */
+    .openapi-disclosure-trigger-label {
+        @apply relative ring-1 bg-tint-base right-0;
+    }
+    .openapi-disclosure-trigger-label span {
+        @apply block;
+    }
+    .openapi-disclosure-trigger {
+        @apply pr-3;
+    }
+}
+
+.openapi-disclosure-trigger[aria-expanded="true"] svg {
+    @apply rotate-45;
+}
+
+.openapi-disclosure-trigger[aria-expanded="false"] {
+    @apply w-auto;
 }
 
 .openapi-section-body.openapi-schema.openapi-schema-root {
@@ -819,8 +860,18 @@ body:has(.openapi-select-popover) {
     @apply p-2.5;
 }
 
+.openapi-schema-alternatives {
+    @apply ml-1.5 pl-3 border-l border-tint-subtle;
+}
+.openapi-schema-alternative {
+    @apply relative;
+}
+.openapi-schema-alternative-separator {
+    @apply p-0.5 tracking-wide leading-none uppercase text-[0.625rem] text-tint-subtle whitespace-nowrap absolute -left-3 -bottom-2.5 -translate-x-1/2 z-10 bg-tint-base border-y border-tint-subtle -rotate-6;
+}
+
 .openapi-tooltip {
-    @apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none font-medium px-1.5 py-0.5 shadow-sm text-[13px];
+    @apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none circular-corners:rounded-lg font-medium px-1.5 py-0.5 shadow-sm text-[13px];
 }
 
 .openapi-tooltip svg {
@@ -865,11 +916,11 @@ body:has(.openapi-select-popover) {
 }
 
 @keyframes popover-leave {
-    0% {
+    from {
         opacity: 1;
         transform: translateY(0) scale(1);
     }
-    100% {
+    to {
         opacity: 0;
         transform: translateY(4px) scale(0.95);
     }
diff --git a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
index e95ffc7545..ccb66babd4 100644
--- a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
+++ b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
@@ -2,6 +2,7 @@ import type { DocumentBlockReusableContent } from '@gitbook/api';
 
 import { resolveContentRef } from '@/lib/references';
 
+import type { GitBookSpaceContext } from '@v2/lib/context';
 import { getDataOrNull } from '@v2/lib/data';
 import type { BlockProps } from './Block';
 import { UnwrappedBlocks } from './Blocks';
@@ -13,15 +14,28 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
         throw new Error('Expected a content context to render a reusable content block');
     }
 
-    const resolved = await resolveContentRef(block.data.ref, context.contentContext);
-    if (!resolved?.reusableContent?.document) {
+    const dataFetcher = block.meta?.token
+        ? context.contentContext.dataFetcher.withToken({ apiToken: block.meta.token })
+        : context.contentContext.dataFetcher;
+
+    const resolved = await resolveContentRef(block.data.ref, {
+        ...context.contentContext,
+        dataFetcher,
+    });
+
+    if (!resolved?.reusableContent) {
+        return null;
+    }
+
+    const reusableContent = resolved.reusableContent.revisionReusableContent;
+    if (!reusableContent.document) {
         return null;
     }
 
     const document = await getDataOrNull(
-        context.contentContext.dataFetcher.getDocument({
-            spaceId: context.contentContext.space.id,
-            documentId: resolved.reusableContent.document,
+        dataFetcher.getDocument({
+            spaceId: resolved.reusableContent.space.id,
+            documentId: reusableContent.document,
         })
     );
 
@@ -29,12 +43,34 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
         return null;
     }
 
+    // Create a new context for reusable content block, including
+    // the data fetcher with the token from the block meta and the correct
+    // space and revision pointers.
+    const reusableContentContext: GitBookSpaceContext =
+        context.contentContext.space.id === resolved.reusableContent.space.id
+            ? context.contentContext
+            : {
+                  ...context.contentContext,
+                  dataFetcher,
+                  space: resolved.reusableContent.space,
+                  revisionId: resolved.reusableContent.revision,
+                  // When the reusable content is in a different space, we don't resolve relative links to pages
+                  // as this space might not be part of the current site.
+                  // In the future, we might expand the logic to look up the space from the list of all spaces in the site
+                  // and adapt the relative links to point to the correct variant.
+                  pages: [],
+                  shareKey: undefined,
+              };
+
     return (
         <UnwrappedBlocks
             nodes={document.nodes}
             document={document}
             ancestorBlocks={[...ancestorBlocks, block]}
-            context={context}
+            context={{
+                ...context,
+                contentContext: reusableContentContext,
+            }}
         />
     );
 }
diff --git a/packages/gitbook/src/components/DocumentView/StepperStep.tsx b/packages/gitbook/src/components/DocumentView/StepperStep.tsx
index 46e70ed603..5c7456ad60 100644
--- a/packages/gitbook/src/components/DocumentView/StepperStep.tsx
+++ b/packages/gitbook/src/components/DocumentView/StepperStep.tsx
@@ -34,13 +34,13 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
             <div className="relative select-none">
                 <div
                     className={tcls(
-                        'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-subtle tabular-nums',
-                        'font-medium text-primary'
+                        'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-solid theme-muted:bg-primary-subtle tabular-nums',
+                        'font-medium text-contrast-primary-solid theme-muted:text-primary'
                     )}
                 >
                     {index + 1}
                 </div>
-                <div className="can-override-bg absolute top-9 bottom-2 left-[0.875rem] w-px bg-primary-subtle" />
+                <div className="can-override-bg absolute top-9 bottom-2 left-[0.875rem] w-px bg-primary-7 theme-muted:bg-primary-subtle" />
             </div>
             <Blocks
                 {...contextProps}
diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
index 2a246d13c6..49aa7a1bc5 100644
--- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
@@ -4,13 +4,14 @@ import {
     SiteInsightsLinkPosition,
 } from '@gitbook/api';
 
-import { Link } from '@/components/primitives';
+import { LinkBox, LinkOverlay } from '@/components/primitives';
 import { Image } from '@/components/utils';
 import { resolveContentRef } from '@/lib/references';
-import { type ClassValue, tcls } from '@/lib/tailwind';
+import { tcls } from '@/lib/tailwind';
 
 import { RecordColumnValue } from './RecordColumnValue';
 import type { TableRecordKV, TableViewProps } from './Table';
+import { RecordCardStyles } from './styles';
 import { getRecordValue } from './utils';
 
 export async function RecordCard(
@@ -23,18 +24,18 @@ export async function RecordCard(
     const coverFile = view.coverDefinition
         ? getRecordValue<string[]>(record[1], view.coverDefinition)?.[0]
         : null;
-    const cover =
-        coverFile && context.contentContext
-            ? await resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
-            : null;
-
     const targetRef = view.targetDefinition
         ? (record[1].values[view.targetDefinition] as ContentRef)
         : null;
-    const target =
+
+    const [cover, target] = await Promise.all([
+        coverFile && context.contentContext
+            ? resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
+            : null,
         targetRef && context.contentContext
-            ? await resolveContentRef(targetRef, context.contentContext)
-            : null;
+            ? resolveContentRef(targetRef, context.contentContext)
+            : null,
+    ]);
 
     const coverIsSquareOrPortrait =
         cover?.file?.dimensions &&
@@ -44,15 +45,15 @@ export async function RecordCard(
         <div
             className={tcls(
                 'grid-area-1-1',
-                'z-0',
                 'relative',
                 'grid',
                 'bg-tint-base',
                 'w-[calc(100%+2px)]',
                 'h-[calc(100%+2px)]',
                 'inset-[-1px]',
-                'rounded-[7px]',
+                'rounded',
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-xl',
                 'overflow-hidden',
                 '[&_.heading>div:first-child]:hidden',
                 '[&_.heading>div]:text-[.8em]',
@@ -143,45 +144,26 @@ export async function RecordCard(
         </div>
     );
 
-    const style = [
-        'group',
-        'grid',
-        'shadow-1xs',
-        'shadow-tint-9/1',
-        'rounded-md',
-        'straight-corners:rounded-none',
-        'dark:shadow-transparent',
-        'z-0',
-
-        'before:pointer-events-none',
-        'before:grid-area-1-1',
-        'before:transition-shadow',
-        'before:w-full',
-        'before:h-full',
-        'before:rounded-[inherit]',
-        'before:ring-1',
-        'before:ring-tint-12/2',
-        'before:z-10',
-        'before:relative',
-    ] as ClassValue;
-
     if (target && targetRef) {
         return (
-            <Link
-                href={target.href}
-                className={tcls(style, 'hover:before:ring-tint-12/5')}
-                insights={{
-                    type: 'link_click',
-                    link: {
-                        target: targetRef,
-                        position: SiteInsightsLinkPosition.Content,
-                    },
-                }}
-            >
+            // We don't use `Link` directly here because we could end up in a situation where
+            // a link is rendered inside a link, which is not allowed in HTML.
+            // It causes an hydration error in React.
+            <LinkBox href={target.href} classNames={['RecordCardStyles']}>
+                <LinkOverlay
+                    href={target.href}
+                    insights={{
+                        type: 'link_click',
+                        link: {
+                            target: targetRef,
+                            position: SiteInsightsLinkPosition.Content,
+                        },
+                    }}
+                />
                 {body}
-            </Link>
+            </LinkBox>
         );
     }
 
-    return <div className={tcls(style)}>{body}</div>;
+    return <div className={tcls(RecordCardStyles)}>{body}</div>;
 }
diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx
index d5ddcdc491..50dc0d8270 100644
--- a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx
@@ -115,7 +115,8 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
                 return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
             }
 
-            const alignment = getColumnAlignment(definition);
+            const horizontalAlignment = getColumnAlignment(definition);
+            const childrenHorizontalAlignment = `[&_*]:${horizontalAlignment}`;
 
             return (
                 <Blocks
@@ -130,8 +131,8 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
                         'lg:space-y-3',
                         'leading-normal',
                         verticalAlignment,
-                        alignment === 'right' ? 'text-right' : null,
-                        alignment === 'center' ? 'text-center' : null,
+                        horizontalAlignment,
+                        childrenHorizontalAlignment,
                     ]}
                     context={context}
                     blockStyle={['w-full', 'max-w-[unset]']}
diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx
index e39f386cc5..b09760ceb2 100644
--- a/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx
@@ -15,14 +15,14 @@ export function RecordRow(
         fixedColumns: string[];
     }
 ) {
-    const { view, autoSizedColumns, fixedColumns, block } = props;
+    const { view, autoSizedColumns, fixedColumns, block, context } = props;
 
     return (
         <div className={styles.row} role="row">
             {view.columns.map((column) => {
                 const columnWidth = getColumnWidth({
                     column,
-                    columnWidths: view.columnWidths,
+                    columnWidths: context.mode === 'print' ? undefined : view.columnWidths,
                     autoSizedColumns,
                     fixedColumns,
                 });
diff --git a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
index 65bae8e0b8..606b2305b5 100644
--- a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
+++ b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx
@@ -13,10 +13,10 @@ import { getColumnAlignment } from './utils';
      3. Auto-size is turned off without setting a width, we then default to a fixed width of 100px
 */
 export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
-    const { block, view, records, style } = props;
+    const { block, view, records, style, context } = props;
 
     /* Calculate how many columns are auto-sized vs fixed width */
-    const columnWidths = view.columnWidths;
+    const columnWidths = context.mode === 'print' ? undefined : view.columnWidths;
     const autoSizedColumns = view.columns.filter((column) => !columnWidths?.[column]);
     const fixedColumns = view.columns.filter((column) => columnWidths?.[column]);
 
@@ -38,36 +38,33 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
                         className={tcls(
                             tableWidth,
                             styles.rowGroup,
-                            'straight-corners:rounded-none'
+                            'straight-corners:rounded-none',
+                            'circular-corners:rounded-xl'
                         )}
                     >
                         <div role="row" className={tcls('flex', 'w-full')}>
-                            {view.columns.map((column) => {
-                                const alignment = getColumnAlignment(block.data.definition[column]);
-                                return (
-                                    <div
-                                        key={column}
-                                        role="columnheader"
-                                        className={tcls(
-                                            styles.columnHeader,
-                                            alignment === 'right' ? 'text-right' : null,
-                                            alignment === 'center' ? 'text-center' : null
-                                        )}
-                                        style={{
-                                            width: getColumnWidth({
-                                                column,
-                                                columnWidths,
-                                                autoSizedColumns,
-                                                fixedColumns,
-                                            }),
-                                            minWidth: columnWidths?.[column] || '100px',
-                                        }}
-                                        title={block.data.definition[column].title}
-                                    >
-                                        {block.data.definition[column].title}
-                                    </div>
-                                );
-                            })}
+                            {view.columns.map((column) => (
+                                <div
+                                    key={column}
+                                    role="columnheader"
+                                    className={tcls(
+                                        styles.columnHeader,
+                                        getColumnAlignment(block.data.definition[column])
+                                    )}
+                                    style={{
+                                        width: getColumnWidth({
+                                            column,
+                                            columnWidths,
+                                            autoSizedColumns,
+                                            fixedColumns,
+                                        }),
+                                        minWidth: columnWidths?.[column] || '100px',
+                                    }}
+                                    title={block.data.definition[column].title}
+                                >
+                                    {block.data.definition[column].title}
+                                </div>
+                            ))}
                         </div>
                     </div>
                 )}
diff --git a/packages/gitbook/src/components/DocumentView/Table/styles.ts b/packages/gitbook/src/components/DocumentView/Table/styles.ts
new file mode 100644
index 0000000000..3e33b06837
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/Table/styles.ts
@@ -0,0 +1,26 @@
+import type { ClassValue } from '@/lib/tailwind';
+
+export const RecordCardStyles = [
+    'group',
+    'grid',
+    'shadow-1xs',
+    'shadow-tint-9/1',
+    'depth-flat:shadow-none',
+    'rounded',
+    'straight-corners:rounded-none',
+    'circular-corners:rounded-xl',
+    'dark:shadow-transparent',
+
+    'before:pointer-events-none',
+    'before:grid-area-1-1',
+    'before:transition-shadow',
+    'before:w-full',
+    'before:h-full',
+    'before:rounded-[inherit]',
+    'before:ring-1',
+    'before:ring-tint-12/2',
+    'before:z-10',
+    'before:relative',
+
+    'hover:before:ring-tint-12/5',
+] as ClassValue;
diff --git a/packages/gitbook/src/components/DocumentView/Table/table.module.css b/packages/gitbook/src/components/DocumentView/Table/table.module.css
index 4b602a607c..53065b9d67 100644
--- a/packages/gitbook/src/components/DocumentView/Table/table.module.css
+++ b/packages/gitbook/src/components/DocumentView/Table/table.module.css
@@ -23,7 +23,7 @@
 }
 
 .columnHeader {
-    @apply text-sm font-medium py-2 px-4 text-tint-strong;
+    @apply text-sm font-medium py-2 px-3 text-tint-strong;
 }
 
 .row {
diff --git a/packages/gitbook/src/components/DocumentView/Table/utils.ts b/packages/gitbook/src/components/DocumentView/Table/utils.ts
index 01bd82bcd6..1fc95b357e 100644
--- a/packages/gitbook/src/components/DocumentView/Table/utils.ts
+++ b/packages/gitbook/src/components/DocumentView/Table/utils.ts
@@ -1,4 +1,5 @@
 import type { ContentRef, DocumentTableDefinition, DocumentTableRecord } from '@gitbook/api';
+import assertNever from 'assert-never';
 
 /**
  * Get the value for a column in a record.
@@ -14,11 +15,24 @@ export function getRecordValue<T extends number | string | boolean | string[] |
 /**
  * Get the text alignment for a column.
  */
-export function getColumnAlignment(column: DocumentTableDefinition): 'left' | 'right' | 'center' {
+export function getColumnAlignment(column: DocumentTableDefinition) {
+    const defaultAlignment = 'text-left';
+
     if (column.type === 'text') {
-        return column.textAlignment ?? 'left';
+        switch (column.textAlignment) {
+            case undefined:
+            case 'left':
+                return defaultAlignment;
+            case 'center':
+                return 'text-center';
+            case 'right':
+                return 'text-right';
+            default:
+                assertNever(column.textAlignment);
+        }
     }
-    return 'left';
+
+    return defaultAlignment;
 }
 
 /**
diff --git a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx
index 38afbcf086..e4465b3032 100644
--- a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx
+++ b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx
@@ -5,6 +5,8 @@ import React, { useCallback, useMemo } from 'react';
 import { useHash, useIsMounted } from '@/components/hooks';
 import * as storage from '@/lib/local-storage';
 import { type ClassValue, tcls } from '@/lib/tailwind';
+import type { DocumentBlockTabs } from '@gitbook/api';
+import { HashLinkButton, hashLinkButtonWrapperStyles } from '../HashLinkButton';
 
 interface TabsState {
     activeIds: {
@@ -68,9 +70,10 @@ export function DynamicTabs(
     props: TabsInput & {
         tabsBody: React.ReactNode[];
         style: ClassValue;
+        block: DocumentBlockTabs;
     }
 ) {
-    const { id, tabs, tabsBody, style } = props;
+    const { id, block, tabs, tabsBody, style } = props;
 
     const hash = useHash();
     const [tabsState, setTabsState] = useTabsState();
@@ -146,8 +149,8 @@ export function DynamicTabs(
                 'ring-inset',
                 'ring-tint-subtle',
                 'flex',
-                'overflow-hidden',
                 'flex-col',
+                'overflow-hidden',
                 style
             )}
         >
@@ -165,16 +168,14 @@ export function DynamicTabs(
                 )}
             >
                 {tabs.map((tab) => (
-                    <button
+                    <div
                         key={tab.id}
-                        role="tab"
-                        aria-selected={active.id === tab.id}
-                        aria-controls={getTabPanelId(tab.id)}
-                        id={getTabButtonId(tab.id)}
-                        onClick={() => {
-                            onSelectTab(tab);
-                        }}
                         className={tcls(
+                            hashLinkButtonWrapperStyles,
+                            'flex',
+                            'items-center',
+                            'gap-3.5',
+
                             //prev from active-tab
                             '[&:has(+_.active-tab)]:rounded-br-md',
 
@@ -184,14 +185,6 @@ export function DynamicTabs(
                             //next from active-tab
                             '[.active-tab_+_:after]:rounded-br-md',
 
-                            'inline-block',
-                            'text-sm',
-                            'px-3.5',
-                            'py-2',
-                            'transition-[color]',
-                            'font-[500]',
-                            'relative',
-
                             'after:transition-colors',
                             'after:border-r',
                             'after:absolute',
@@ -202,14 +195,16 @@ export function DynamicTabs(
                             'after:h-[70%]',
                             'after:w-[1px]',
 
+                            'px-3.5',
+                            'py-2',
+
                             'last:after:border-transparent',
 
                             'text-tint',
                             'bg-tint-12/1',
                             'hover:text-tint-strong',
-
-                            'truncate',
                             'max-w-full',
+                            'truncate',
 
                             active.id === tab.id
                                 ? [
@@ -224,8 +219,34 @@ export function DynamicTabs(
                                 : null
                         )}
                     >
-                        {tab.title}
-                    </button>
+                        <button
+                            type="button"
+                            role="tab"
+                            aria-selected={active.id === tab.id}
+                            aria-controls={getTabPanelId(tab.id)}
+                            id={getTabButtonId(tab.id)}
+                            onClick={() => {
+                                onSelectTab(tab);
+                            }}
+                            className={tcls(
+                                'inline-block',
+                                'text-sm',
+                                'transition-[color]',
+                                'font-[500]',
+                                'relative',
+                                'max-w-full',
+                                'truncate'
+                            )}
+                        >
+                            {tab.title}
+                        </button>
+
+                        <HashLinkButton
+                            id={getTabButtonId(tab.id)}
+                            block={block}
+                            label="Direct link to tab"
+                        />
+                    </div>
                 ))}
             </div>
             {tabs.map((tab, index) => (
diff --git a/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx b/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx
index 4d5a0d555a..d1ab244852 100644
--- a/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx
+++ b/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx
@@ -39,6 +39,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
                     <DynamicTabs
                         key={tab.id}
                         id={block.key!}
+                        block={block}
                         tabs={[tab]}
                         tabsBody={[tabsBody[index]]}
                         style={style}
@@ -48,5 +49,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
         );
     }
 
-    return <DynamicTabs id={block.key!} tabs={tabs} tabsBody={tabsBody} style={style} />;
+    return (
+        <DynamicTabs id={block.key!} block={block} tabs={tabs} tabsBody={tabsBody} style={style} />
+    );
 }
diff --git a/packages/gitbook/src/components/DocumentView/spacing.ts b/packages/gitbook/src/components/DocumentView/spacing.ts
index a11db3546c..1c21a8b17d 100644
--- a/packages/gitbook/src/components/DocumentView/spacing.ts
+++ b/packages/gitbook/src/components/DocumentView/spacing.ts
@@ -10,6 +10,8 @@ export function getBlockTextStyle(block: DocumentBlock): {
     lineHeight: string;
     /** Tailwind class for the margin top (mt-*) */
     marginTop?: string;
+    /** Tailwind class for the margin top to apply on the anchor link button */
+    anchorButtonMarginTop?: string;
 } {
     switch (block.type) {
         case 'paragraph':
@@ -22,18 +24,21 @@ export function getBlockTextStyle(block: DocumentBlock): {
                 textSize: 'text-3xl font-semibold',
                 lineHeight: 'leading-tight',
                 marginTop: 'mt-[1em]',
+                anchorButtonMarginTop: 'mt-[1.05em]',
             };
         case 'heading-2':
             return {
                 textSize: 'text-2xl font-semibold',
                 lineHeight: 'leading-snug',
                 marginTop: 'mt-[0.75em]',
+                anchorButtonMarginTop: 'mt-[0.9em]',
             };
         case 'heading-3':
             return {
                 textSize: 'text-xl font-semibold',
                 lineHeight: 'leading-snug',
                 marginTop: 'mt-[0.5em]',
+                anchorButtonMarginTop: 'mt-[0.65em]',
             };
         case 'divider':
             return {
diff --git a/packages/gitbook/src/components/Footer/Footer.tsx b/packages/gitbook/src/components/Footer/Footer.tsx
index 776c0a4af4..454ac0f421 100644
--- a/packages/gitbook/src/components/Footer/Footer.tsx
+++ b/packages/gitbook/src/components/Footer/Footer.tsx
@@ -25,6 +25,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
 
     return (
         <footer
+            id="site-footer"
             className={tcls(
                 'border-tint-subtle border-t',
                 // If the footer only contains a mode toggle, we only show it on smaller screens
@@ -35,12 +36,16 @@ export function Footer(props: { context: GitBookSiteContext }) {
                 <div className={tcls(CONTAINER_STYLE, 'px-4', 'py-8', 'lg:py-12', 'mx-auto')}>
                     <div
                         className={tcls(
-                            'mx-auto grid max-w-3xl justify-between gap-12 lg:max-w-none',
+                            'lg:!max-w-none mx-auto grid max-w-3xl site-full-width:max-w-screen-2xl justify-between gap-12',
                             'grid-cols-[auto_auto]',
                             'lg:grid-cols-[18rem_minmax(auto,_48rem)_auto]',
                             'xl:grid-cols-[18rem_minmax(auto,_48rem)_14rem]',
+                            'site-full-width:lg:grid-cols-[18rem_minmax(auto,_80rem)_auto]',
+                            'site-full-width:xl:grid-cols-[18rem_minmax(auto,_80rem)_14rem]',
                             'page-no-toc:lg:grid-cols-[minmax(auto,_48rem)_auto]',
-                            'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]'
+                            'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]',
+                            '[body:has(.site-full-width,.page-no-toc)_&]:lg:grid-cols-[minmax(auto,_90rem)_auto]',
+                            '[body:has(.site-full-width,.page-no-toc)_&]:xl:grid-cols-[14rem_minmax(auto,_90rem)_14rem]'
                         )}
                     >
                         {
@@ -101,7 +106,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
                                         'col-span-2 page-has-toc:lg:col-span-1 page-has-toc:lg:col-start-2 page-no-toc:xl:col-span-1 page-no-toc:xl:col-start-2'
                                     )}
                                 >
-                                    <div className="mx-auto flex max-w-3xl flex-col gap-10 sm:flex-row sm:gap-6">
+                                    <div className="mx-auto flex max-w-3xl site-full-width:max-w-screen-2xl flex-col gap-10 sm:flex-row sm:gap-6">
                                         {partition(customization.footer.groups, FOOTER_COLUMNS).map(
                                             (column, columnIndex) => (
                                                 <div
diff --git a/packages/gitbook/src/components/Header/Dropdown.tsx b/packages/gitbook/src/components/Header/Dropdown.tsx
deleted file mode 100644
index 21b626ac2c..0000000000
--- a/packages/gitbook/src/components/Header/Dropdown.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { Icon } from '@gitbook/icons';
-import { type DetailedHTMLProps, type HTMLAttributes, useId } from 'react';
-
-import { type ClassValue, tcls } from '@/lib/tailwind';
-
-import { Link, type LinkInsightsProps } from '../primitives';
-
-export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit<
-    Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>,
-    'ref'
->;
-
-/**
- * Button with a dropdown.
- */
-export function Dropdown<E extends HTMLElement>(props: {
-    /** Content of the button */
-    button: (buttonProps: DropdownButtonProps<E>) => React.ReactNode;
-    /** Content of the dropdown */
-    children: React.ReactNode;
-    /** Custom styles */
-    className?: ClassValue;
-}) {
-    const { button, children, className } = props;
-    const dropdownId = useId();
-
-    return (
-        <div className={tcls('group/dropdown', 'relative flex min-w-0 shrink')}>
-            {button({
-                id: dropdownId,
-                tabIndex: 0,
-                'aria-expanded': true,
-                'aria-haspopup': true,
-            })}
-            <div
-                tabIndex={-1}
-                role="menu"
-                aria-orientation="vertical"
-                aria-labelledby={dropdownId}
-                className={tcls(
-                    'w-52',
-                    'max-h-80',
-                    'flex',
-                    'absolute',
-                    'top-full',
-                    'left-0',
-                    'origin-top-left',
-                    'invisible',
-                    'transition-opacity',
-                    'duration-1000',
-                    'group-hover/dropdown:visible',
-                    'group-focus-within/dropdown:visible',
-                    className
-                )}
-            >
-                <div className="fixed z-50 w-52">
-                    <div
-                        className={tcls(
-                            'mt-2',
-                            'w-full',
-                            'max-h-80',
-                            'bg-tint-base',
-                            'rounded-lg',
-                            'straight-corners:rounded-sm',
-                            'p-2',
-                            'shadow-1xs',
-                            'overflow-auto',
-                            'ring-1',
-                            'ring-tint-subtle',
-                            'focus:outline-none'
-                        )}
-                    >
-                        {children}
-                    </div>
-                </div>
-            </div>
-        </div>
-    );
-}
-
-/**
- * Animated chevron to display in the dropdown button.
- */
-export function DropdownChevron() {
-    return (
-        <Icon
-            icon="chevron-down"
-            className={tcls(
-                'shrink-0',
-                'opacity-6',
-                'size-3',
-                'ms-1',
-                'transition-all',
-                'group-hover/dropdown:opacity-11',
-                'group-focus-within/dropdown:rotate-180'
-            )}
-        />
-    );
-}
-
-/**
- * Group of menu items in a dropdown.
- */
-export function DropdownMenu(props: { children: React.ReactNode }) {
-    const { children } = props;
-
-    return <div className={tcls('flex', 'flex-col', 'gap-1')}>{children}</div>;
-}
-
-/**
- * Menu item in a dropdown.
- */
-export function DropdownMenuItem(
-    props: {
-        href: string | null;
-        active?: boolean;
-        className?: ClassValue;
-        children: React.ReactNode;
-    } & LinkInsightsProps
-) {
-    const { children, active = false, href, className, insights } = props;
-
-    if (href) {
-        return (
-            <Link
-                href={href}
-                insights={insights}
-                className={tcls(
-                    'rounded straight-corners:rounded-sm px-3 py-1 text-sm',
-                    active ? 'bg-primary text-primary-strong' : null,
-                    'hover:bg-tint-hover',
-                    className
-                )}
-            >
-                {children}
-            </Link>
-        );
-    }
-
-    return (
-        <div className={tcls('px-3 py-1 font-medium text-tint text-xs', className)}>{children}</div>
-    );
-}
diff --git a/packages/gitbook/src/components/Header/DropdownMenu.tsx b/packages/gitbook/src/components/Header/DropdownMenu.tsx
new file mode 100644
index 0000000000..8f51e4685c
--- /dev/null
+++ b/packages/gitbook/src/components/Header/DropdownMenu.tsx
@@ -0,0 +1,176 @@
+'use client';
+
+import { Icon } from '@gitbook/icons';
+import type { DetailedHTMLProps, HTMLAttributes } from 'react';
+import { useState } from 'react';
+
+import { type ClassValue, tcls } from '@/lib/tailwind';
+
+import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
+
+import { Link, type LinkInsightsProps } from '../primitives';
+
+export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit<
+    Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>,
+    'ref'
+>;
+
+/**
+ * Button with a dropdown.
+ */
+export function DropdownMenu(props: {
+    /** Content of the button */
+    button: React.ReactNode;
+    /** Content of the dropdown */
+    children: React.ReactNode;
+    /** Custom styles */
+    className?: ClassValue;
+    /** Open the dropdown on hover */
+    openOnHover?: boolean;
+}) {
+    const { button, children, className, openOnHover = false } = props;
+    const [hovered, setHovered] = useState(false);
+    const [clicked, setClicked] = useState(false);
+
+    return (
+        <RadixDropdownMenu.Root
+            modal={false}
+            open={openOnHover ? clicked || hovered : clicked}
+            onOpenChange={setClicked}
+        >
+            <RadixDropdownMenu.Trigger
+                asChild
+                onMouseEnter={() => setHovered(true)}
+                onMouseLeave={() => setHovered(false)}
+                onClick={() => (openOnHover ? setClicked(!clicked) : null)}
+                className="group/dropdown"
+            >
+                {button}
+            </RadixDropdownMenu.Trigger>
+
+            <RadixDropdownMenu.Portal>
+                <RadixDropdownMenu.Content
+                    data-testid="dropdown-menu"
+                    hideWhenDetached
+                    collisionPadding={8}
+                    onMouseEnter={() => setHovered(true)}
+                    onMouseLeave={() => setHovered(false)}
+                    align="start"
+                    className="z-40 animate-present pt-2"
+                >
+                    <div
+                        className={tcls(
+                            'flex max-h-80 min-w-40 max-w-[40vw] flex-col gap-1 overflow-auto rounded-lg straight-corners:rounded-sm bg-tint-base p-2 shadow-lg ring-1 ring-tint-subtle sm:min-w-52 sm:max-w-80',
+                            className
+                        )}
+                    >
+                        {children}
+                    </div>
+                </RadixDropdownMenu.Content>
+            </RadixDropdownMenu.Portal>
+        </RadixDropdownMenu.Root>
+    );
+}
+
+/**
+ * Animated chevron to display in the dropdown button.
+ */
+export function DropdownChevron() {
+    return (
+        <Icon
+            icon="chevron-down"
+            className={tcls(
+                'shrink-0',
+                'opacity-6',
+                'size-3',
+                'ms-1',
+                'transition-all',
+                'group-hover/dropdown:opacity-11',
+                'group-data-[state=open]/dropdown:opacity-11',
+                'group-data-[state=open]/dropdown:rotate-180'
+            )}
+        />
+    );
+}
+
+/**
+ * Button with a chevron for use in dropdowns.
+ */
+export function DropdownButton(props: {
+    children: React.ReactNode;
+    className?: ClassValue;
+}) {
+    const { children, className } = props;
+
+    return (
+        <div className={tcls('group/dropdown', 'flex', 'items-center', className)}>
+            {children}
+            <DropdownChevron />
+        </div>
+    );
+}
+
+/**
+ * Menu item in a dropdown.
+ */
+export function DropdownMenuItem(
+    props: {
+        href: string | null;
+        active?: boolean;
+        className?: ClassValue;
+        children: React.ReactNode;
+    } & LinkInsightsProps
+) {
+    const { children, active = false, href, className, insights } = props;
+
+    const itemClassName = tcls(
+        'rounded straight-corners:rounded-sm px-3 py-1 text-sm',
+        active
+            ? 'bg-primary text-primary-strong data-[highlighted]:bg-primary-hover'
+            : 'data-[highlighted]:bg-tint-hover',
+        'focus:outline-none',
+        className
+    );
+
+    if (href) {
+        return (
+            <RadixDropdownMenu.Item asChild>
+                <Link href={href} insights={insights} className={itemClassName}>
+                    {children}
+                </Link>
+            </RadixDropdownMenu.Item>
+        );
+    }
+
+    return (
+        <RadixDropdownMenu.Item
+            className={tcls('px-3 py-1 font-medium text-tint text-xs', className)}
+        >
+            {children}
+        </RadixDropdownMenu.Item>
+    );
+}
+
+export function DropdownSubMenu(props: { children: React.ReactNode; label: React.ReactNode }) {
+    const { children, label } = props;
+
+    return (
+        <RadixDropdownMenu.Sub>
+            <RadixDropdownMenu.SubTrigger className="flex cursor-pointer items-center justify-between rounded straight-corners:rounded-sm px-3 py-1 text-sm focus:outline-none data-[highlighted]:bg-tint-hover">
+                {label}
+                <Icon icon="chevron-right" className="size-3 shrink-0 opacity-6" />
+            </RadixDropdownMenu.SubTrigger>
+            <RadixDropdownMenu.Portal>
+                <RadixDropdownMenu.SubContent
+                    hideWhenDetached
+                    collisionPadding={8}
+                    className="z-40 animate-present"
+                >
+                    <div className="flex max-h-80 min-w-40 max-w-[40vw] flex-col gap-1 overflow-auto rounded-lg straight-corners:rounded-sm bg-tint-base p-2 shadow-lg ring-1 ring-tint-subtle sm:min-w-52 sm:max-w-80">
+                        {children}
+                    </div>
+                </RadixDropdownMenu.SubContent>
+            </RadixDropdownMenu.Portal>
+        </RadixDropdownMenu.Sub>
+    );
+}
diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx
index 35c2583c41..f3bd145d56 100644
--- a/packages/gitbook/src/components/Header/Header.tsx
+++ b/packages/gitbook/src/components/Header/Header.tsx
@@ -104,11 +104,9 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
                                           'lg:basis-40',
                                           'md:max-w-[40%]',
                                           'lg:max-w-lg',
-                                          'lg:ml-[max(calc((100%-18rem-48rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - margin (3rem)
+                                          'lg:ml-[max(calc((100%-18rem-48rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem)
                                           'xl:ml-[max(calc((100%-18rem-48rem-14rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - outline (14rem) - margin (3rem)
                                           'page-no-toc:lg:ml-[max(calc((100%-18rem-48rem-18rem-3rem)/2),0rem)]',
-                                          'page-full-width:lg:ml-[max(calc((100%-18rem-103rem-3rem)/2),1.5rem)]',
-                                          'page-full-width:2xl:ml-[max(calc((100%-18rem-96rem-14rem+3rem)/2),1.5rem)]',
                                           'md:mr-auto',
                                           'order-last',
                                           'md:order-[unset]',
@@ -195,7 +193,6 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
                         <div
                             className={tcls(
                                 CONTAINER_STYLE,
-                                'page-default-width:max-w-[unset]',
                                 'grow',
                                 'flex',
                                 'items-end',
@@ -205,7 +202,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
                             {siteSpaces.length > 1 && (
                                 <div
                                     id="variants"
-                                    className="my-2 mr-5 page-no-toc:flex hidden grow border-tint border-r pr-5 *:grow only:mr-0 only:border-none only:pr-0 sm:max-w-64"
+                                    className="my-2 mr-5 grow border-tint border-r pr-5 *:grow only:mr-0 only:border-none only:pr-0 sm:max-w-64"
                                 >
                                     <SpacesDropdown
                                         context={context}
diff --git a/packages/gitbook/src/components/Header/HeaderLink.tsx b/packages/gitbook/src/components/Header/HeaderLink.tsx
index 184537c15d..6b13fef714 100644
--- a/packages/gitbook/src/components/Header/HeaderLink.tsx
+++ b/packages/gitbook/src/components/Header/HeaderLink.tsx
@@ -13,12 +13,11 @@ import { tcls } from '@/lib/tailwind';
 
 import { Button, Link } from '../primitives';
 import {
-    Dropdown,
     type DropdownButtonProps,
     DropdownChevron,
     DropdownMenu,
     DropdownMenuItem,
-} from './Dropdown';
+} from './DropdownMenu';
 
 export async function HeaderLink(props: {
     context: GitBookSiteContext;
@@ -33,21 +32,13 @@ export async function HeaderLink(props: {
 
     if (link.links && link.links.length > 0) {
         return (
-            <Dropdown
+            <DropdownMenu
                 className={`shrink ${customization.styling.search === 'prominent' ? 'right-0 left-auto' : null}`}
-                button={(buttonProps) => {
-                    if (!target || !link.to) {
-                        return (
-                            <HeaderItemDropdown
-                                {...buttonProps}
-                                headerPreset={headerPreset}
-                                title={link.title}
-                            />
-                        );
-                    }
-                    return (
+                button={
+                    !target || !link.to ? (
+                        <HeaderItemDropdown headerPreset={headerPreset} title={link.title} />
+                    ) : (
                         <HeaderLinkNavItem
-                            {...buttonProps}
                             linkTarget={link.to}
                             linkStyle={linkStyle}
                             headerPreset={headerPreset}
@@ -55,15 +46,14 @@ export async function HeaderLink(props: {
                             isDropdown
                             href={target?.href}
                         />
-                    );
-                }}
+                    )
+                }
+                openOnHover={true}
             >
-                <DropdownMenu>
-                    {link.links.map((subLink, index) => (
-                        <SubHeaderLink key={index} {...props} link={subLink} />
-                    ))}
-                </DropdownMenu>
-            </Dropdown>
+                {link.links.map((subLink, index) => (
+                    <SubHeaderLink key={index} {...props} link={subLink} />
+                ))}
+            </DropdownMenu>
         );
     }
 
@@ -157,17 +147,19 @@ function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPrese
 
         'text-tint',
         'links-default:hover:text-primary',
+        'links-default:data-[state=open]:text-primary',
         'links-default:tint:hover:text-tint-strong',
-
+        'links-default:tint:data-[state=open]:text-tint-strong',
         'underline-offset-2',
         'links-accent:hover:underline',
+        'links-accent:data-[state=open]:underline',
         'links-accent:underline-offset-4',
         'links-accent:decoration-primary-subtle',
         'links-accent:decoration-[3px]',
         'links-accent:py-0.5', // Prevent underline from being cut off at the bottom
 
         'theme-bold:text-header-link',
-        'theme-bold:hover:text-header-link'
+        'theme-bold:hover:!text-header-link/7'
     );
 }
 
diff --git a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx
index 5d27fc9a8d..7954b80ec4 100644
--- a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx
+++ b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx
@@ -10,7 +10,7 @@ import type React from 'react';
 import { resolveContentRef } from '@/lib/references';
 import { tcls } from '@/lib/tailwind';
 
-import { Dropdown, DropdownChevron, DropdownMenu, DropdownMenuItem } from './Dropdown';
+import { DropdownChevron, DropdownMenu, DropdownMenuItem, DropdownSubMenu } from './DropdownMenu';
 import styles from './headerLinks.module.css';
 
 /**
@@ -23,7 +23,7 @@ export function HeaderLinkMore(props: {
 }) {
     const { label, links, context } = props;
 
-    const renderButton = () => (
+    const renderButton = (
         <button
             type="button"
             className={tcls(
@@ -45,19 +45,18 @@ export function HeaderLinkMore(props: {
 
     return (
         <div className={`${styles.linkEllipsis} z-20 items-center`}>
-            <Dropdown
+            <DropdownMenu
                 button={renderButton}
+                openOnHover={true}
                 className={tcls(
                     'max-md:right-0 max-md:left-auto',
                     context.customization.styling.search === 'prominent' && 'right-0 left-auto'
                 )}
             >
-                <DropdownMenu>
-                    {links.map((link, index) => (
-                        <MoreMenuLink key={index} link={link} context={context} />
-                    ))}
-                </DropdownMenu>
-            </Dropdown>
+                {links.map((link, index) => (
+                    <MoreMenuLink key={index} link={link} context={context} />
+                ))}
+            </DropdownMenu>
         </div>
     );
 }
@@ -70,32 +69,28 @@ async function MoreMenuLink(props: {
 
     const target = link.to ? await resolveContentRef(link.to, context) : null;
 
-    return (
-        <>
-            {'links' in link && link.links.length > 0 && (
-                <hr className="-mx-2 my-1 border-tint border-t first:hidden" />
-            )}
-            <DropdownMenuItem
-                href={target?.href ?? null}
-                insights={
-                    link.to
-                        ? {
-                              type: 'link_click',
-                              link: {
-                                  target: link.to,
-                                  position: SiteInsightsLinkPosition.Header,
-                              },
-                          }
-                        : undefined
-                }
-            >
-                {link.title}
-            </DropdownMenuItem>
-            {'links' in link
-                ? link.links.map((subLink, index) => (
-                      <MoreMenuLink key={index} {...props} link={subLink} />
-                  ))
-                : null}
-        </>
+    return 'links' in link && link.links.length > 0 ? (
+        <DropdownSubMenu label={link.title}>
+            {link.links.map((subLink, index) => {
+                return <MoreMenuLink key={index} {...props} link={subLink} />;
+            })}
+        </DropdownSubMenu>
+    ) : (
+        <DropdownMenuItem
+            href={target?.href ?? null}
+            insights={
+                link.to
+                    ? {
+                          type: 'link_click',
+                          link: {
+                              target: link.to,
+                              position: SiteInsightsLinkPosition.Header,
+                          },
+                      }
+                    : undefined
+            }
+        >
+            {link.title}
+        </DropdownMenuItem>
     );
 }
diff --git a/packages/gitbook/src/components/Header/HeaderLogo.tsx b/packages/gitbook/src/components/Header/HeaderLogo.tsx
index 08a1d2d10a..72d0b4798b 100644
--- a/packages/gitbook/src/components/Header/HeaderLogo.tsx
+++ b/packages/gitbook/src/components/Header/HeaderLogo.tsx
@@ -20,7 +20,7 @@ export async function HeaderLogo(props: HeaderLogoProps) {
 
     return (
         <Link
-            href={linker.toAbsoluteURL(linker.toPathInSpace(''))}
+            href={linker.toAbsoluteURL(linker.toPathInSite(''))}
             className={tcls('group/headerlogo', 'min-w-0', 'shrink', 'flex', 'items-center')}
         >
             {customization.header.logo ? (
@@ -48,8 +48,6 @@ export async function HeaderLogo(props: HeaderLogoProps) {
                     ]}
                     priority="high"
                     style={tcls(
-                        'rounded',
-                        'straight-corners:rounded-sm',
                         'overflow-hidden',
                         'shrink',
                         'min-w-0',
diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
index 39e81e59f4..87ad44b5d7 100644
--- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx
+++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx
@@ -1,10 +1,10 @@
 import type { SiteSpace } from '@gitbook/api';
 
+import { getSiteSpaceURL } from '@/lib/sites';
 import { tcls } from '@/lib/tailwind';
-
 import type { GitBookSiteContext } from '@v2/lib/context';
-import { Dropdown, DropdownChevron, DropdownMenu } from './Dropdown';
-import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
+import { DropdownChevron, DropdownMenu } from './DropdownMenu';
+import { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem';
 
 export function SpacesDropdown(props: {
     context: GitBookSiteContext;
@@ -13,17 +13,15 @@ export function SpacesDropdown(props: {
     className?: string;
 }) {
     const { context, siteSpace, siteSpaces, className } = props;
-    const { linker } = context;
 
     return (
-        <Dropdown
+        <DropdownMenu
             className={tcls(
                 'group-hover/dropdown:invisible', // Prevent hover from opening the dropdown, as it's annoying in this context
                 'group-focus-within/dropdown:group-hover/dropdown:visible' // When the dropdown is already open, it should remain visible when hovered
             )}
-            button={(buttonProps) => (
+            button={
                 <div
-                    {...buttonProps}
                     data-testid="space-dropdown-button"
                     className={tcls(
                         'flex',
@@ -40,49 +38,41 @@ export function SpacesDropdown(props: {
                         'straight-corners:rounded-none',
 
                         'bg-tint-base',
-                        'group-hover/dropdown:bg-tint-base',
-                        'group-focus-within/dropdown:bg-tint-base',
 
                         'text-sm',
                         'text-tint',
-                        'group-hover/dropdown:text-tint-strong',
-                        'group-focus-within/dropdown:text-tint-strong',
+                        'hover:text-tint-strong',
+                        'data-[state=open]:text-tint-strong',
 
                         'ring-1',
                         'ring-tint-subtle',
-                        'group-hover/dropdown:ring-tint-hover',
-                        'group-focus-within/dropdown:ring-tint-hover',
+                        'hover:ring-tint-hover',
+                        'data-[state=open]:ring-tint-hover',
 
                         'contrast-more:bg-tint-base',
                         'contrast-more:ring-1',
-                        'contrast-more:group-hover/dropdown:ring-2',
+                        'contrast-more:hover:ring-2',
+                        'contrast-more:data-[state=open]:ring-2',
                         'contrast-more:ring-tint',
-                        'contrast-more:group-hover/dropdown:ring-tint-hover',
-                        'contrast-more:group-focus-within/dropdown:ring-tint-hover',
+                        'contrast-more:hover:ring-tint-hover',
+                        'contrast-more:data-[state=open]:ring-tint-hover',
 
                         className
                     )}
                 >
-                    <span className={tcls('line-clamp-1', 'grow')}>{siteSpace.title}</span>
+                    <span className={tcls('truncate', 'grow')}>{siteSpace.title}</span>
                     <DropdownChevron />
                 </div>
-            )}
+            }
         >
-            <DropdownMenu>
-                {siteSpaces.map((otherSiteSpace, index) => (
-                    <SpacesDropdownMenuItem
-                        key={`${otherSiteSpace.id}-${index}`}
-                        variantSpace={{
-                            id: otherSiteSpace.id,
-                            title: otherSiteSpace.title,
-                            url: otherSiteSpace.urls.published
-                                ? linker.toLinkForContent(otherSiteSpace.urls.published)
-                                : otherSiteSpace.space.urls.app,
-                        }}
-                        active={otherSiteSpace.id === siteSpace.id}
-                    />
-                ))}
-            </DropdownMenu>
-        </Dropdown>
+            <SpacesDropdownMenuItems
+                slimSpaces={siteSpaces.map((space) => ({
+                    id: space.id,
+                    title: space.title,
+                    url: getSiteSpaceURL(context, space),
+                }))}
+                curPath={siteSpace.path}
+            />
+        </DropdownMenu>
     );
 }
diff --git a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx
index 8c3676ff89..2044d0fc6f 100644
--- a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx
+++ b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx
@@ -4,14 +4,30 @@ import type { Space } from '@gitbook/api';
 
 import { joinPath } from '@/lib/paths';
 import { useCurrentPagePath } from '../hooks';
-import { DropdownMenuItem } from './Dropdown';
+import { DropdownMenuItem } from './DropdownMenu';
 
-function useVariantSpaceHref(variantSpaceUrl: string) {
+interface VariantSpace {
+    id: Space['id'];
+    title: Space['title'];
+    url: string;
+}
+
+// When switching to a different variant space, we reconstruct the URL by swapping the space path.
+function useVariantSpaceHref(variantSpaceUrl: string, currentSpacePath: string, active = false) {
     const currentPathname = useCurrentPagePath();
 
+    // We need to ensure that the variant space URL is not the same as the current space path.
+    // If it is, we return only the variant space URL to redirect to the root of the variant space.
+    // This is necessary in case the currentPathname is the same as the variantSpaceUrl,
+    // otherwise we would redirect to the same space if the variant space that we are switching to is the default one.
+    if (!active && currentPathname.startsWith(`${currentSpacePath}/`)) {
+        return variantSpaceUrl;
+    }
+
     if (URL.canParse(variantSpaceUrl)) {
         const targetUrl = new URL(variantSpaceUrl);
         targetUrl.pathname = joinPath(targetUrl.pathname, currentPathname);
+
         targetUrl.searchParams.set('fallback', 'true');
 
         return targetUrl.toString();
@@ -22,11 +38,12 @@ function useVariantSpaceHref(variantSpaceUrl: string) {
 }
 
 export function SpacesDropdownMenuItem(props: {
-    variantSpace: { id: Space['id']; title: Space['title']; url: string };
+    variantSpace: VariantSpace;
     active: boolean;
+    currentSpacePath: string;
 }) {
-    const { variantSpace, active } = props;
-    const variantHref = useVariantSpaceHref(variantSpace.url);
+    const { variantSpace, active, currentSpacePath } = props;
+    const variantHref = useVariantSpaceHref(variantSpace.url, currentSpacePath, active);
 
     return (
         <DropdownMenuItem key={variantSpace.id} href={variantHref} active={active}>
@@ -34,3 +51,23 @@ export function SpacesDropdownMenuItem(props: {
         </DropdownMenuItem>
     );
 }
+
+export function SpacesDropdownMenuItems(props: {
+    slimSpaces: VariantSpace[];
+    curPath: string;
+}) {
+    const { slimSpaces, curPath } = props;
+
+    return (
+        <>
+            {slimSpaces.map((space) => (
+                <SpacesDropdownMenuItem
+                    key={space.id}
+                    variantSpace={space}
+                    active={false}
+                    currentSpacePath={curPath}
+                />
+            ))}
+        </>
+    );
+}
diff --git a/packages/gitbook/src/components/PDF/PDFPage.tsx b/packages/gitbook/src/components/PDF/PDFPage.tsx
index 96cb784ccd..d2bc5c91a6 100644
--- a/packages/gitbook/src/components/PDF/PDFPage.tsx
+++ b/packages/gitbook/src/components/PDF/PDFPage.tsx
@@ -9,7 +9,6 @@ import {
 } from '@gitbook/api';
 import { Icon } from '@gitbook/icons';
 import type { GitBookSiteContext, GitBookSpaceContext } from '@v2/lib/context';
-import { getPageDocument } from '@v2/lib/data';
 import type { GitBookLinker } from '@v2/lib/links';
 import type { Metadata } from 'next';
 import { notFound } from 'next/navigation';
@@ -29,6 +28,7 @@ import { PageControlButtons } from './PageControlButtons';
 import { PrintButton } from './PrintButton';
 import './pdf.css';
 import { sanitizeGitBookAppURL } from '@/lib/app';
+import { getPageDocument } from '@v2/lib/data';
 
 const DEFAULT_LIMIT = 100;
 
@@ -53,7 +53,7 @@ export async function PDFPage(props: {
 }) {
     const baseContext = props.context;
     const searchParams = new URLSearchParams(props.searchParams);
-    const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams));
+    const pdfParams = getPDFSearchParams(searchParams);
 
     const customization =
         'customization' in baseContext ? baseContext.customization : defaultCustomization();
@@ -224,8 +224,7 @@ async function PDFPageDocument(props: {
     context: GitBookSpaceContext;
 }) {
     const { page, context } = props;
-    const { space } = context;
-    const document = await getPageDocument(context.dataFetcher, space, page);
+    const document = await getPageDocument(context, page);
 
     return (
         <PrintPage id={getPagePDFContainerId(page)}>
@@ -246,6 +245,7 @@ async function PDFPageDocument(props: {
                             page,
                         },
                         getId: (id) => getPagePDFContainerId(page, id),
+                        shouldRenderLinkPreviews: false, // We don't want to render link previews in the PDF.
                     }}
                     // We consider all pages as offscreen in PDF mode
                     // to ensure we can efficiently render as many pages as possible
diff --git a/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx b/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx
index 41775ca782..4b18d3d909 100644
--- a/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx
+++ b/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx
@@ -30,6 +30,7 @@ export function AsideSectionHighlight({
 
                 'rounded-md',
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-2xl',
                 'sidebar-list-line:rounded-l-none',
 
                 'sidebar-list-pill:bg-primary',
diff --git a/packages/gitbook/src/components/PageAside/PageAside.tsx b/packages/gitbook/src/components/PageAside/PageAside.tsx
index a51410cbc0..d00af1bf2d 100644
--- a/packages/gitbook/src/components/PageAside/PageAside.tsx
+++ b/packages/gitbook/src/components/PageAside/PageAside.tsx
@@ -48,11 +48,9 @@ export function PageAside(props: {
                 'group/aside',
                 'hidden',
                 'xl:flex',
-                // 'page-no-toc:lg:flex',
                 'flex-col',
                 'basis-56',
-                // 'page-no-toc:basis-40',
-                // 'page-no-toc:xl:basis-56',
+                'xl:ml-12',
                 'grow-0',
                 'shrink-0',
                 'break-anywhere', // To prevent long words in headings from breaking the layout
@@ -87,7 +85,7 @@ export function PageAside(props: {
                 'page-api-block:xl:max-2xl:dark:hover:shadow-tint-1/1',
                 'page-api-block:xl:max-2xl:rounded-md',
                 'page-api-block:xl:max-2xl:h-auto',
-                'page-api-block:xl:max-2xl:my-8',
+                'page-api-block:xl:max-2xl:my-4',
                 'page-api-block:p-2'
             )}
         >
diff --git a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx
index d9b02ebdf8..519c1ea15b 100644
--- a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx
+++ b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx
@@ -82,6 +82,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
 
                             'rounded-md',
                             'straight-corners:rounded-none',
+                            'circular-corners:rounded-2xl',
                             'sidebar-list-line:rounded-l-none',
 
                             'hover:bg-tint-hover',
diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx
index 6d548dec95..9b9e744743 100644
--- a/packages/gitbook/src/components/PageBody/PageBody.tsx
+++ b/packages/gitbook/src/components/PageBody/PageBody.tsx
@@ -4,7 +4,7 @@ import React from 'react';
 
 import { getSpaceLanguage } from '@/intl/server';
 import { t } from '@/intl/translate';
-import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
+import { hasFullWidthBlock, hasMoreThan, isNodeEmpty } from '@/lib/document';
 import type { AncestorRevisionPage } from '@/lib/pages';
 import { tcls } from '@/lib/tailwind';
 import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
@@ -17,6 +17,8 @@ import { PageFooterNavigation } from './PageFooterNavigation';
 import { PageHeader } from './PageHeader';
 import { PreservePageLayout } from './PreservePageLayout';
 
+const LINK_PREVIEW_MAX_COUNT = 100;
+
 export function PageBody(props: {
     context: GitBookSiteContext;
     page: RevisionPageDocument;
@@ -27,7 +29,18 @@ export function PageBody(props: {
     const { page, context, ancestors, document, withPageFeedback } = props;
     const { customization } = context;
 
-    const asFullWidth = document ? hasFullWidthBlock(document) : false;
+    const contentFullWidth = document ? hasFullWidthBlock(document) : false;
+
+    // Render link previews only if there are less than LINK_PREVIEW_MAX_COUNT links in the document.
+    const shouldRenderLinkPreviews = document
+        ? !hasMoreThan(
+              document,
+              (inline) => inline.object === 'inline' && inline.type === 'link',
+              LINK_PREVIEW_MAX_COUNT
+          )
+        : false;
+    const pageFullWidth = page.id === 'wtthNFMqmEQmnt5LKR0q';
+    const asFullWidth = pageFullWidth || contentFullWidth;
     const language = getSpaceLanguage(customization);
     const updatedAt = page.updatedAt ?? page.createdAt;
 
@@ -36,15 +49,11 @@ export function PageBody(props: {
             <main
                 className={tcls(
                     'relative min-w-0 flex-1',
-                    'py-8 lg:px-12',
+                    'mx-auto max-w-screen-2xl py-8',
                     // Allow words to break if they are too long.
                     'break-anywhere',
-                    // When in api page mode without the aside, we align with the border of the main content
-                    'page-api-block:xl:max-2xl:pr-0',
-                    // Max size to ensure one column in api is aligned with rest of content (2 x 3xl) + (gap-3 + 2) * px-12
-                    'page-api-block:mx-auto page-api-block:max-w-screen-2xl',
-                    // page.layout.tableOfContents ? null : 'xl:ml-56',
-                    asFullWidth ? 'page-full-width' : 'page-default-width',
+                    pageFullWidth ? 'page-full-width 2xl:px-8' : 'page-default-width',
+                    asFullWidth ? 'site-full-width' : 'site-default-width',
                     page.layout.tableOfContents ? 'page-has-toc' : 'page-no-toc'
                 )}
             >
@@ -70,6 +79,7 @@ export function PageBody(props: {
                             context={{
                                 mode: 'default',
                                 contentContext: context,
+                                shouldRenderLinkPreviews,
                             }}
                         />
                     </React.Suspense>
@@ -81,7 +91,7 @@ export function PageBody(props: {
                     <PageFooterNavigation context={context} page={page} />
                 ) : null}
 
-                <div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong">
+                <div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl page-full-width:max-w-screen-2xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong">
                     {updatedAt ? (
                         <p className="mr-auto text-sm">
                             {t(language, 'page_last_modified', <DateRelative value={updatedAt} />)}
diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx
index d434d5eba9..3b37dfd389 100644
--- a/packages/gitbook/src/components/PageBody/PageCover.tsx
+++ b/packages/gitbook/src/components/PageBody/PageCover.tsx
@@ -21,7 +21,10 @@ export async function PageCover(props: {
     context: GitBookSiteContext;
 }) {
     const { as, page, cover, context } = props;
-    const resolved = cover.ref ? await resolveContentRef(cover.ref, context) : null;
+    const [resolved, resolvedDark] = await Promise.all([
+        cover.ref ? resolveContentRef(cover.ref, context) : null,
+        cover.refDark ? resolveContentRef(cover.refDark, context) : null,
+    ]);
 
     return (
         <div
@@ -33,14 +36,20 @@ export async function PageCover(props: {
                     ? [
                           'sm:-mx-6',
                           'md:-mx-8',
-                          '-lg:mr-8',
-                          'lg:ml-0',
+                          'lg:-mr-8',
+                          'lg:-ml-12',
                           !page.layout.tableOfContents &&
                           context.customization.header.preset !== 'none'
-                              ? 'lg:-ml-64'
+                              ? 'xl:-ml-[19rem]'
                               : null,
                       ]
-                    : ['sm:mx-auto', 'max-w-3xl', 'sm:rounded-md', 'mb-8']
+                    : [
+                          'sm:mx-auto',
+                          'max-w-3xl ',
+                          'page-full-width:max-w-screen-2xl',
+                          'sm:rounded-md',
+                          'mb-8',
+                      ]
             )}
         >
             <Image
@@ -58,6 +67,12 @@ export async function PageCover(props: {
                                   height: defaultPageCover.height,
                               },
                           },
+                    dark: resolvedDark
+                        ? {
+                              src: resolvedDark.href,
+                              size: resolvedDark.file?.dimensions,
+                          }
+                        : null,
                 }}
                 resize={
                     // When using the default cover, we don't want to resize as it's a SVG
diff --git a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx
index 620cb5970d..a6be37d412 100644
--- a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx
+++ b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx
@@ -32,8 +32,8 @@ export async function PageFooterNavigation(props: {
                 'mt-6',
                 'gap-2',
                 'max-w-3xl',
+                'page-full-width:max-w-screen-2xl',
                 'mx-auto',
-                'page-api-block:ml-0',
                 'text-tint'
             )}
         >
@@ -107,6 +107,7 @@ function NavigationCard(
                 'border',
                 'border-tint-subtle',
                 'rounded',
+                'circular-corners:rounded-2xl',
                 'straight-corners:rounded-none',
                 'hover:border-primary',
                 'text-pretty',
diff --git a/packages/gitbook/src/components/PageBody/PageHeader.tsx b/packages/gitbook/src/components/PageBody/PageHeader.tsx
index b8b62f945d..7c1ad9cbf7 100644
--- a/packages/gitbook/src/components/PageBody/PageHeader.tsx
+++ b/packages/gitbook/src/components/PageBody/PageHeader.tsx
@@ -23,7 +23,14 @@ export async function PageHeader(props: {
 
     return (
         <header
-            className={tcls('max-w-3xl', 'mx-auto', 'mb-6', 'space-y-3', 'page-api-block:ml-0')}
+            className={tcls(
+                'max-w-3xl',
+                'page-full-width:max-w-screen-2xl',
+                'mx-auto',
+                'mb-6',
+                'space-y-3',
+                'page-api-block:ml-0'
+            )}
         >
             {ancestors.length > 0 && (
                 <nav>
diff --git a/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx b/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx
index 7094711fbb..06a801034c 100644
--- a/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx
+++ b/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx
@@ -3,12 +3,12 @@ import * as React from 'react';
 
 /**
  * This component preserves the layout of the page while loading a new one.
- * This approach is needed as page layout (full width block) is done using CSS (`body:has(.page-full-width)`),
+ * This approach is needed as page layout (full width block) is done using CSS (`body:has(.full-width)`),
  * which becomes false while transitioning between the 2 page states:
  *
- * 1. Page 1 with full width block: `body:has(.page-full-width)` is true
- * 2. Loading skeleton while transitioning to page 2: `body:has(.page-full-width)` is false
- * 3. Page 2 with full width block: `body:has(.page-full-width)` is true
+ * 1. Page 1 with full width block: `body:has(.site-full-width)` is true
+ * 2. Loading skeleton while transitioning to page 2: `body:has(.site-full-width)` is false
+ * 3. Page 2 with full width block: `body:has(.site-full-width)` is true
  *
  * This component ensures that the layout is preserved while transitioning between the 2 page states (in step 2).
  */
@@ -24,9 +24,9 @@ export function PreservePageLayout(props: { asFullWidth: boolean }) {
         }
 
         if (asFullWidth) {
-            header.classList.add('page-full-width');
+            header.classList.add('site-full-width');
         } else {
-            header.classList.remove('page-full-width');
+            header.classList.remove('site-full-width');
         }
     }, [asFullWidth]);
 
diff --git a/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx b/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx
index 551a8b83f5..2c15f52831 100644
--- a/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx
+++ b/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx
@@ -64,10 +64,10 @@ export function PageFeedbackForm(props: {
                 <div className="rounded-full border border-tint-subtle bg-tint-base contrast-more:border-tint-12">
                     <div className="flex">
                         <RatingButton
-                            rating={PageFeedbackRating.Bad}
-                            label={tString(languages, 'was_this_helpful_negative')}
-                            onClick={() => onSubmitRating(PageFeedbackRating.Bad)}
-                            active={rating === PageFeedbackRating.Bad}
+                            rating={PageFeedbackRating.Good}
+                            label={tString(languages, 'was_this_helpful_positive')}
+                            onClick={() => onSubmitRating(PageFeedbackRating.Good)}
+                            active={rating === PageFeedbackRating.Good}
                             disabled={rating !== undefined}
                         />
                         <RatingButton
@@ -78,10 +78,10 @@ export function PageFeedbackForm(props: {
                             disabled={rating !== undefined}
                         />
                         <RatingButton
-                            rating={PageFeedbackRating.Good}
-                            label={tString(languages, 'was_this_helpful_positive')}
-                            onClick={() => onSubmitRating(PageFeedbackRating.Good)}
-                            active={rating === PageFeedbackRating.Good}
+                            rating={PageFeedbackRating.Bad}
+                            label={tString(languages, 'was_this_helpful_negative')}
+                            onClick={() => onSubmitRating(PageFeedbackRating.Bad)}
+                            active={rating === PageFeedbackRating.Bad}
                             disabled={rating !== undefined}
                         />
                     </div>
diff --git a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
index c7d2f35b6e..ffaad1fff8 100644
--- a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
+++ b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
@@ -1,5 +1,4 @@
 import {
-    CustomizationCorners,
     CustomizationHeaderPreset,
     CustomizationIconsStyle,
     CustomizationSidebarBackgroundStyle,
@@ -75,16 +74,15 @@ export async function CustomizationRootLayout(props: {
             lang={customization.internationalization.locale}
             className={tcls(
                 customization.header.preset === CustomizationHeaderPreset.None
-                    ? 'site-header-none'
+                    ? null
                     : 'scroll-pt-[76px]', // Take the sticky header in consideration for the scrolling
-                customization.styling.corners === CustomizationCorners.Straight
-                    ? ' straight-corners'
-                    : '',
+                customization.styling.corners && `${customization.styling.corners}-corners`,
                 'theme' in customization.styling && `theme-${customization.styling.theme}`,
                 tintColor ? ' tint' : 'no-tint',
                 sidebarStyles.background && `sidebar-${sidebarStyles.background}`,
                 sidebarStyles.list && `sidebar-list-${sidebarStyles.list}`,
                 'links' in customization.styling && `links-${customization.styling.links}`,
+                'depth' in customization.styling && `depth-${customization.styling.depth}`,
                 fontNotoColorEmoji.variable,
                 ibmPlexMono.variable,
                 fontData.type === 'default' ? fontData.variable : 'font-custom',
diff --git a/packages/gitbook/src/components/RootLayout/globals.css b/packages/gitbook/src/components/RootLayout/globals.css
index 71dae7cc6b..a46758c0ae 100644
--- a/packages/gitbook/src/components/RootLayout/globals.css
+++ b/packages/gitbook/src/components/RootLayout/globals.css
@@ -143,9 +143,12 @@
             margin-right: 0;
             width: calc(100% - var(--scrollbar-width));
         }
-        body:has(.page-full-width) .scroll-nojump {
-            margin-left: 0;
-            width: 100%;
+    }
+
+    .elevate-link {
+        & a[href]:not(.link-overlay) {
+            position: relative;
+            z-index: 20;
         }
     }
 }
@@ -161,6 +164,6 @@ html.dark {
     color-scheme: dark light;
 }
 
-html.announcement-hidden .announcement-banner {
+html.announcement-hidden #announcement-banner {
     @apply hidden;
 }
diff --git a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx
index 8a7dce493d..f7c4b1fc8e 100644
--- a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx
+++ b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx
@@ -176,6 +176,7 @@ function AnswerFollowupQuestions(props: { followupQuestions: string[] }) {
                         'py-2',
                         'rounded',
                         'straight-corners:rounded-none',
+                        'circular-corners:rounded-full',
                         'text-tint',
                         'hover:bg-tint-hover',
                         'focus-within:bg-tint-hover'
diff --git a/packages/gitbook/src/components/Search/SearchButton.tsx b/packages/gitbook/src/components/Search/SearchButton.tsx
index ab8516d059..7bcc4f7481 100644
--- a/packages/gitbook/src/components/Search/SearchButton.tsx
+++ b/packages/gitbook/src/components/Search/SearchButton.tsx
@@ -45,21 +45,25 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV
                 'w-full',
                 'py-2',
                 'px-3',
+                'circular-corners:px-4',
                 'gap-2',
 
                 'bg-tint-base',
 
                 'ring-1',
                 'ring-tint-12/2',
+                'depth-flat:ring-tint-subtle',
 
                 'shadow-sm',
                 'shadow-tint-12/3',
                 'dark:shadow-none',
+                'depth-flat:shadow-none',
 
                 'text-tint',
 
                 'rounded-lg',
                 'straight-corners:rounded-sm',
+                'circular-corners:rounded-full',
 
                 'contrast-more:ring-tint-12',
                 'contrast-more:text-tint-strong',
@@ -68,10 +72,12 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV
                 'hover:bg-tint-subtle',
                 'hover:shadow-md',
                 'hover:scale-102',
+                'depth-flat:hover:scale-100',
                 'hover:ring-tint-hover',
                 'hover:text-tint-strong',
                 'focus:shadow-md',
                 'focus:scale-102',
+                'depth-flat:focus:scale-100',
                 'focus:ring-tint-hover',
                 'focus:text-tint-strong',
 
diff --git a/packages/gitbook/src/components/Search/SearchModal.tsx b/packages/gitbook/src/components/Search/SearchModal.tsx
index bdc747241a..c8046ca333 100644
--- a/packages/gitbook/src/components/Search/SearchModal.tsx
+++ b/packages/gitbook/src/components/Search/SearchModal.tsx
@@ -225,9 +225,11 @@ function SearchModalBody(
                 'w-full',
                 'rounded-lg',
                 'straight-corners:rounded-sm',
+                'circular-corners:rounded-2xl',
                 'ring-1',
-                'ring-tint-hover',
+                'ring-tint',
                 'shadow-2xl',
+                'depth-flat:shadow-none',
                 'overflow-hidden',
                 'dark:ring-inset',
                 'dark:ring-tint'
diff --git a/packages/gitbook/src/components/Search/SearchPageResultItem.tsx b/packages/gitbook/src/components/Search/SearchPageResultItem.tsx
index 6b4d0e1c0e..aa97e621a4 100644
--- a/packages/gitbook/src/components/Search/SearchPageResultItem.tsx
+++ b/packages/gitbook/src/components/Search/SearchPageResultItem.tsx
@@ -108,6 +108,7 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
                     'p-2',
                     'rounded',
                     'straight-corners:rounded-none',
+                    'circular-corners:rounded-full',
                     active ? ['bg-primary-solid', 'text-contrast-primary-solid'] : ['opacity-6']
                 )}
             >
diff --git a/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx b/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx
index 83f585c82b..787632a805 100644
--- a/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx
+++ b/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx
@@ -72,6 +72,7 @@ export const SearchQuestionResultItem = React.forwardRef(function SearchQuestion
                     'rounded',
                     'self-center',
                     'straight-corners:rounded-none',
+                    'circular-corners:rounded-full',
                     active ? ['bg-primary-solid', 'text-contrast-primary-solid'] : ['opacity-6']
                 )}
             >
diff --git a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
index ee2daa1ffb..8ccda684d2 100644
--- a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
+++ b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx
@@ -66,17 +66,14 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
                         <HighlightQuery query={query} text={item.title} />
                     </p>
                 ) : null}
-                {item.body ? (
-                    <p className={tcls('text-sm', 'line-clamp-3', 'relative')}>
-                        <HighlightQuery query={query} text={item.body} />
-                    </p>
-                ) : null}
+                {item.body ? highlightQueryInBody(item.body, query) : null}
             </div>
             <div
                 className={tcls(
                     'p-2',
                     'rounded',
                     'straight-corners:rounded-none',
+                    'circular-corners:rounded-full',
                     'bg-primary-solid',
                     'text-contrast-primary-solid',
                     'hidden',
@@ -89,3 +86,14 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
         </Link>
     );
 });
+
+function highlightQueryInBody(body: string, query: string) {
+    const idx = body.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
+
+    // Ensure the query to be highlighted is visible in the body.
+    return (
+        <p className={tcls('text-sm', 'line-clamp-3', 'relative')}>
+            <HighlightQuery query={query} text={idx < 20 ? body : `...${body.slice(idx - 10)}`} />
+        </p>
+    );
+}
diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx
index b45bc0aba1..9bd8257c16 100644
--- a/packages/gitbook/src/components/Search/server-actions.tsx
+++ b/packages/gitbook/src/components/Search/server-actions.tsx
@@ -345,6 +345,7 @@ async function transformAnswer(
                         mode: 'default',
                         contentContext: undefined,
                         wrapBlocksInSuspense: false,
+                        shouldRenderLinkPreviews: false, // We don't want to render link previews in the AI answer.
                     }}
                     style={['space-y-5']}
                 />
diff --git a/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx b/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx
index 096024089e..cc689bbef1 100644
--- a/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx
+++ b/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx
@@ -18,7 +18,7 @@ export function RocketLoaderDetector(props: { nonce?: string }) {
             alert.className = 'p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 mt-8 mx-8';
             alert.innerHTML = \`
               <strong>Error in site configuration:</strong>
-                It looks like \${window.location.hostname} has been incorrectly configured in Cloudflare. This may lead to unexpected behavior or issues with the page loading. If you are the owner of this site, please refer to <a href="https://docs.gitbook.com/published-documentation/custom-domain/configure-dns#are-you-using-cloudflare" class="underline">GitBook's documentation</a> for steps to fix the problem.
+                It looks like \${window.location.hostname} has been incorrectly configured in Cloudflare. This may lead to unexpected behavior or issues with the page loading. If you are the owner of this site, please refer to <a href="https://gitbook.com/docs/published-documentation/custom-domain/configure-dns#are-you-using-cloudflare" class="underline">GitBook's documentation</a> for steps to fix the problem.
               \`;
 
             document.body.prepend(alert);
diff --git a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
index dda7095266..83211354f1 100644
--- a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
+++ b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
@@ -102,30 +102,37 @@ export async function generateSiteLayoutMetadata(context: GitBookSiteContext): P
     const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
 
     const faviconSize = 48;
-    const icons = [
-        {
-            url: customIcon?.light
-                ? await getResizedImageURL(imageResizer, customIcon.light, {
-                      width: faviconSize,
-                      height: faviconSize,
-                  })
-                : linker.toAbsoluteURL(
-                      linker.toPathInSpace('~gitbook/icon?size=small&theme=light')
-                  ),
-            type: 'image/png',
-            media: '(prefers-color-scheme: light)',
-        },
-        {
-            url: customIcon?.dark
-                ? await getResizedImageURL(imageResizer, customIcon.dark, {
-                      width: faviconSize,
-                      height: faviconSize,
-                  })
-                : linker.toAbsoluteURL(linker.toPathInSpace('~gitbook/icon?size=small&theme=dark')),
-            type: 'image/png',
-            media: '(prefers-color-scheme: dark)',
-        },
-    ];
+    const icons = await Promise.all(
+        [
+            {
+                url: customIcon?.light
+                    ? getResizedImageURL(imageResizer, customIcon.light, {
+                          width: faviconSize,
+                          height: faviconSize,
+                      })
+                    : linker.toAbsoluteURL(
+                          linker.toPathInSpace('~gitbook/icon?size=small&theme=light')
+                      ),
+                type: 'image/png',
+                media: '(prefers-color-scheme: light)',
+            },
+            {
+                url: customIcon?.dark
+                    ? getResizedImageURL(imageResizer, customIcon.dark, {
+                          width: faviconSize,
+                          height: faviconSize,
+                      })
+                    : linker.toAbsoluteURL(
+                          linker.toPathInSpace('~gitbook/icon?size=small&theme=dark')
+                      ),
+                type: 'image/png',
+                media: '(prefers-color-scheme: dark)',
+            },
+        ].map(async (icon) => ({
+            ...icon,
+            url: await icon.url,
+        }))
+    );
 
     return {
         title: site.title,
diff --git a/packages/gitbook/src/components/SitePage/SitePage.tsx b/packages/gitbook/src/components/SitePage/SitePage.tsx
index fe6934a529..9e7c08515f 100644
--- a/packages/gitbook/src/components/SitePage/SitePage.tsx
+++ b/packages/gitbook/src/components/SitePage/SitePage.tsx
@@ -62,7 +62,7 @@ export async function SitePage(props: SitePageProps) {
     const withSections = Boolean(sections && sections.list.length > 0);
     const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader };
 
-    const document = await getPageDocument(context.dataFetcher, context.space, page);
+    const document = await getPageDocument(context, page);
 
     return (
         <PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
diff --git a/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx b/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx
index 37d97167c9..7b840afbb0 100644
--- a/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx
+++ b/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx
@@ -19,7 +19,7 @@ export function SitePageSkeleton() {
                 'lg:items-start'
             )}
         >
-            <div className={tcls('flex-1', 'max-w-3xl', 'mx-auto', 'page-full-width:mx-0')}>
+            <div className={tcls('flex-1', 'max-w-3xl', 'mx-auto', 'site-full-width:mx-0')}>
                 <SkeletonHeading style={tcls('mb-8')} />
                 <SkeletonParagraph style={tcls('mb-4')} />
             </div>
diff --git a/packages/gitbook/src/components/SitePage/fetch.ts b/packages/gitbook/src/components/SitePage/fetch.ts
index 6ca4e55293..732c36072c 100644
--- a/packages/gitbook/src/components/SitePage/fetch.ts
+++ b/packages/gitbook/src/components/SitePage/fetch.ts
@@ -69,7 +69,11 @@ async function resolvePage(context: GitBookSiteContext, params: PagePathParams |
 
         // If a page still can't be found, we try with the API, in case we have a redirect at site level.
         const redirectPathname = withLeadingSlash(rawPathname);
-        if (/^\/[a-zA-Z0-9-_.\/]+[a-zA-Z0-9-_.]$/.test(redirectPathname)) {
+        if (
+            /^\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+)*$/.test(
+                redirectPathname
+            )
+        ) {
             const redirectSources = new Set<string>([
                 // Test the pathname relative to the root
                 // For example hello/world -> section/variant/hello/world
diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
index a8b83cfb5a..dc78a3c3c2 100644
--- a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
+++ b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx
@@ -75,16 +75,30 @@ export function SiteSectionListItem(props: {
     const isMounted = useIsMounted();
     React.useEffect(() => {}, [isMounted]); // This updates the useScrollToActiveTOCItem hook once we're mounted, so we can actually scroll to the this item
 
-    const linkRef = React.createRef<HTMLAnchorElement>();
-    useScrollToActiveTOCItem({ linkRef, isActive });
+    const anchorRef = React.createRef<HTMLAnchorElement>();
+    useScrollToActiveTOCItem({ anchorRef, isActive });
 
     return (
         <Link
+            ref={anchorRef}
             href={section.url}
-            ref={linkRef}
             aria-current={isActive && 'page'}
             className={tcls(
-                'group/section-link flex flex-row items-center gap-3 rounded-md straight-corners:rounded-none px-3 py-2 transition-all hover:bg-tint-hover hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint',
+                'group/section-link',
+                'flex',
+                'flex-row',
+                'items-center',
+                'gap-3',
+                'rounded-md',
+                'straight-corners:rounded-none',
+                'circular-corners:rounded-xl',
+                'px-3',
+                'py-2',
+                'transition-all',
+                'hover:bg-tint-hover',
+                'hover:text-tint-strong',
+                'contrast-more:hover:ring-1',
+                'contrast-more:hover:ring-tint',
                 isActive
                     ? 'font-semibold text-primary-subtle hover:bg-primary-hover hover:text-primary contrast-more:text-primary contrast-more:hover:text-primary-strong contrast-more:hover:ring-1 contrast-more:hover:ring-primary-hover'
                     : null,
@@ -121,18 +135,15 @@ export function SiteSectionGroupItem(props: {
 
     const hasDescendants = group.sections.length > 0;
     const isActiveGroup = group.sections.some((section) => section.id === currentSection.id);
-    const [isVisible, setIsVisible] = React.useState(isActiveGroup);
+    const shouldOpen = hasDescendants && isActiveGroup;
+    const [isOpen, setIsOpen] = React.useState(shouldOpen);
 
-    // Update the visibility of the children, if we are navigating to a descendant.
+    // Update the visibility of the children if the group becomes active.
     React.useEffect(() => {
-        if (!hasDescendants) {
-            return;
+        if (shouldOpen) {
+            setIsOpen(shouldOpen);
         }
-
-        setIsVisible((prev) => prev || isActiveGroup);
-    }, [isActiveGroup, hasDescendants]);
-
-    const { show, hide, scope } = useToggleAnimation({ hasDescendants, isVisible });
+    }, [shouldOpen]);
 
     return (
         <>
@@ -141,7 +152,7 @@ export function SiteSectionGroupItem(props: {
                 onClick={(event) => {
                     event.preventDefault();
                     event.stopPropagation();
-                    setIsVisible((prev) => !prev);
+                    setIsOpen((prev) => !prev);
                 }}
                 className={`group/section-link flex w-full flex-row items-center gap-3 rounded-md straight-corners:rounded-none px-3 py-2 text-left transition-all hover:bg-tint-hover hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint ${
                     isActiveGroup
@@ -184,7 +195,7 @@ export function SiteSectionGroupItem(props: {
                         'after:h-7',
                         'hover:bg-tint-active',
                         'hover:text-current',
-                        isActiveGroup ? ['hover:bg-tint-hover'] : []
+                        isActiveGroup && 'hover:bg-tint-hover'
                     )}
                 >
                     <Icon
@@ -201,17 +212,13 @@ export function SiteSectionGroupItem(props: {
                             'group-hover:opacity-11',
                             'contrast-more:opacity-11',
 
-                            isVisible ? ['rotate-90'] : ['rotate-0']
+                            isOpen ? 'rotate-90' : 'rotate-0'
                         )}
                     />
                 </span>
             </button>
             {hasDescendants ? (
-                <motion.div
-                    ref={scope}
-                    className={tcls(isVisible ? null : '[&_ul>li]:opacity-1')}
-                    initial={isVisible ? show : hide}
-                >
+                <Descendants isVisible={isOpen}>
                     {group.sections.map((section) => (
                         <SiteSectionListItem
                             section={section}
@@ -220,8 +227,25 @@ export function SiteSectionGroupItem(props: {
                             className="pl-5"
                         />
                     ))}
-                </motion.div>
+                </Descendants>
             ) : null}
         </>
     );
 }
+
+function Descendants(props: {
+    isVisible: boolean;
+    children: React.ReactNode;
+}) {
+    const { isVisible, children } = props;
+    const { show, hide, scope } = useToggleAnimation(isVisible);
+    return (
+        <motion.div
+            ref={scope}
+            className={isVisible ? undefined : '[&_ul>li]:opacity-1'}
+            initial={isVisible ? show : hide}
+        >
+            {children}
+        </motion.div>
+    );
+}
diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
index 2833f9f31a..8d6df8eafa 100644
--- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
+++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx
@@ -83,6 +83,12 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) {
                                                     )
                                                 }
                                                 asChild
+                                                onClick={(e) => {
+                                                    if (value) {
+                                                        e.preventDefault();
+                                                        e.stopPropagation();
+                                                    }
+                                                }}
                                             >
                                                 <SectionGroupTab
                                                     isActive={isActive}
@@ -127,7 +133,7 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) {
                 }}
             >
                 <NavigationMenu.Viewport
-                    className="relative mt-3 h-[var(--radix-navigation-menu-viewport-height)] w-[calc(100vw_-_2rem)] origin-[top_center] overflow-hidden rounded-lg straight-corners:rounded-sm bg-tint-base shadow-lg shadow-tint-10/6 ring-1 ring-tint-subtle duration-250 data-[state=closed]:duration-150 motion-safe:transition-[width,_height,_transform] data-[state=closed]:motion-safe:animate-scaleOut data-[state=open]:motion-safe:animate-scaleIn md:mx-0 md:w-[var(--radix-navigation-menu-viewport-width)] dark:shadow-tint-1/6"
+                    className="relative mt-3 h-[var(--radix-navigation-menu-viewport-height)] w-[calc(100vw_-_2rem)] origin-[top_center] overflow-hidden rounded-lg straight-corners:rounded-sm bg-tint-base depth-flat:shadow-none shadow-lg shadow-tint-10/6 ring-1 ring-tint-subtle duration-250 data-[state=closed]:duration-150 motion-safe:transition-[width,_height,_transform] data-[state=closed]:motion-safe:animate-scaleOut data-[state=open]:motion-safe:animate-scaleIn md:mx-0 md:w-[var(--radix-navigation-menu-viewport-width)] dark:shadow-tint-1/6"
                     style={{
                         translate:
                             undefined /* don't move this to a Tailwind class as Radix renders viewport incorrectly for a few frames */,
@@ -151,7 +157,7 @@ const SectionTab = React.forwardRef(function SectionTab(
             ref={ref}
             {...rest}
             className={tcls(
-                'group relative my-2 flex select-none items-center justify-between rounded straight-corners:rounded-none px-3 py-1',
+                'group relative my-2 flex select-none items-center justify-between rounded circular-corners:rounded-full straight-corners:rounded-none px-3 py-1',
                 isActive
                     ? 'text-primary-subtle'
                     : 'text-tint hover:bg-tint-hover hover:text-tint-strong'
@@ -180,7 +186,7 @@ const SectionGroupTab = React.forwardRef(function SectionGroupTab(
             ref={ref}
             {...rest}
             className={tcls(
-                'group relative my-2 flex select-none items-center justify-between rounded straight-corners:rounded-none px-3 py-1 transition-colors',
+                'group relative my-2 flex select-none items-center justify-between rounded circular-corners:rounded-full straight-corners:rounded-none px-3 py-1 transition-colors hover:cursor-default',
                 isActive
                     ? 'text-primary-subtle'
                     : 'text-tint hover:bg-tint-hover hover:text-tint-strong'
diff --git a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
index 79a007ecc3..98c22bbb66 100644
--- a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
+++ b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts
@@ -1,5 +1,7 @@
-import type { SiteSection, SiteSectionGroup } from '@gitbook/api';
+import { getSectionURL, getSiteSpaceURL } from '@/lib/sites';
+import type { SiteSection, SiteSectionGroup, SiteSpace } from '@gitbook/api';
 import type { GitBookSiteContext, SiteSections } from '@v2/lib/context';
+import assertNever from 'assert-never';
 
 export type ClientSiteSections = {
     list: (ClientSiteSection | ClientSiteSectionGroup)[];
@@ -26,16 +28,32 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
     const clientSections: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
 
     for (const item of list) {
-        if (item.object === 'site-section-group') {
-            clientSections.push({
-                id: item.id,
-                title: item.title,
-                icon: item.icon,
-                object: item.object,
-                sections: item.sections.map((section) => encodeSection(context, section)),
-            });
-        } else {
-            clientSections.push(encodeSection(context, item));
+        switch (item.object) {
+            case 'site-section-group': {
+                const sections = item.sections
+                    .filter((section) => shouldIncludeSection(context, section))
+                    .map((section) => encodeSection(context, section));
+
+                // Skip empty groups
+                if (sections.length === 0) {
+                    continue;
+                }
+
+                clientSections.push({
+                    id: item.id,
+                    title: item.title,
+                    icon: item.icon,
+                    object: item.object,
+                    sections,
+                });
+                continue;
+            }
+            case 'site-section': {
+                clientSections.push(encodeSection(context, item));
+                continue;
+            }
+            default:
+                assertNever(item, 'Unknown site section object type');
         }
     }
 
@@ -46,13 +64,69 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
 }
 
 function encodeSection(context: GitBookSiteContext, section: SiteSection) {
-    const { linker } = context;
     return {
         id: section.id,
         title: section.title,
         description: section.description,
         icon: section.icon,
         object: section.object,
-        url: section.urls.published ? linker.toLinkForContent(section.urls.published) : '',
+        url: findBestTargetURL(context, section),
     };
 }
+
+/**
+ * Test if a section should be included in the list of sections.
+ */
+function shouldIncludeSection(context: GitBookSiteContext, section: SiteSection) {
+    if (context.site.id !== 'site_JOVzv') {
+        return true;
+    }
+
+    // Testing for a new mode of navigation where the multi-variants section are hidden
+    // if they do not include an equivalent of the current site space.
+
+    // TODO: replace with a proper flag on the section
+    const withNavigateOnlyIfEquivalent = section.id === 'sitesc_4jvEm';
+
+    if (!withNavigateOnlyIfEquivalent) {
+        return true;
+    }
+
+    const { siteSpace: currentSiteSpace } = context;
+    if (section.siteSpaces.length === 1) {
+        return true;
+    }
+    return section.siteSpaces.some((siteSpace) =>
+        areSiteSpacesEquivalent(siteSpace, currentSiteSpace)
+    );
+}
+
+/**
+ * Find the best default site space to navigate to for a givent section:
+ * 1. If we are on the default, continue on the default.
+ * 2. If a site space has the same path as the current one, return it.
+ * 3. Otherwise, return the default one.
+ */
+function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
+    const { siteSpace: currentSiteSpace } = context;
+
+    if (section.siteSpaces.length === 1 || currentSiteSpace.default) {
+        return getSectionURL(context, section);
+    }
+
+    const bestMatch = section.siteSpaces.find((siteSpace) =>
+        areSiteSpacesEquivalent(siteSpace, currentSiteSpace)
+    );
+    if (bestMatch) {
+        return getSiteSpaceURL(context, bestMatch);
+    }
+
+    return getSectionURL(context, section);
+}
+
+/**
+ * Test if 2 site spaces are equivalent.
+ */
+function areSiteSpacesEquivalent(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
+    return siteSpace1.path === siteSpace2.path;
+}
diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
index 6b9aefd929..f69de405e3 100644
--- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
+++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
@@ -73,6 +73,7 @@ export function SpaceLayout(props: {
                             'flex-col',
                             'lg:flex-row',
                             CONTAINER_STYLE,
+                            'site-full-width:max-w-full',
 
                             // Ensure the footer is display below the viewport even if the content is not enough
                             withFooter && 'min-h-[calc(100vh-64px)]',
@@ -124,7 +125,7 @@ export function SpaceLayout(props: {
                                             sections={encodeClientSiteSections(context, sections)}
                                         />
                                     )}
-                                    {isMultiVariants && (
+                                    {isMultiVariants && !sections && (
                                         <SpacesDropdown
                                             context={context}
                                             siteSpace={siteSpace}
diff --git a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx
index ad7fc513e3..4875633ac9 100644
--- a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx
@@ -17,10 +17,14 @@ export async function PageDocumentItem(props: {
     context: GitBookSiteContext;
 }) {
     const { rootPages, page, context } = props;
-    const href = context.linker.toPathForPage({ pages: rootPages, page });
+    let href = context.linker.toPathForPage({ pages: rootPages, page });
+    // toPathForPage can returns an empty path, this will cause all links to point to the current page.
+    if (href === '') {
+        href = '/';
+    }
 
     return (
-        <li className={tcls('flex', 'flex-col')}>
+        <li className="flex flex-col">
             <ToggleableLinkItem
                 href={href}
                 pathname={getPagePath(rootPages, page)}
@@ -52,7 +56,7 @@ export async function PageDocumentItem(props: {
                 }
             >
                 {page.emoji || page.icon ? (
-                    <span className={tcls('flex', 'gap-3', 'items-center')}>
+                    <span className="flex items-center gap-3">
                         <TOCPageIcon page={page} />
                         {page.title}
                     </span>
diff --git a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx
index 884f6eedac..24ea366a2e 100644
--- a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx
@@ -15,27 +15,13 @@ export function PageGroupItem(props: {
     const { rootPages, page, context } = props;
 
     return (
-        <li className={tcls('flex', 'flex-col', 'group/page-group-item')}>
+        <li className="group/page-group-item flex flex-col">
             <div
                 className={tcls(
-                    'flex',
-                    'items-center',
-
-                    'gap-3',
-                    'px-3',
-                    'z-[1]',
-                    'sticky',
-                    '-top-5',
-                    'pt-6',
-                    'group-first/page-group-item:-mt-5',
+                    '-top-5 group-first/page-group-item:-mt-5 sticky z-[1] flex items-center gap-3 px-3 pt-6',
+                    'font-semibold text-xs uppercase tracking-wide',
                     'pb-3', // Add extra padding to make the header fade a bit nicer
                     '-mb-1.5', // Then pull the page items a bit closer, effective bottom padding is 1.5 units / 6px.
-
-                    'text-xs',
-                    'tracking-wide',
-                    'font-semibold',
-                    'uppercase',
-
                     '[mask-image:linear-gradient(rgba(0,0,0,1)_70%,rgba(0,0,0,0))]', // Fade out effect of fixed page items. We want the fade to start past the header, this is a good approximation.
                     'bg-tint-base',
                     'sidebar-filled:bg-tint-subtle',
diff --git a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
index 43c31730c5..c00d1c6216 100644
--- a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
@@ -17,24 +17,7 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Git
         <li className={tcls('flex', 'flex-col')}>
             <Link
                 href={resolved?.href ?? '#'}
-                className={tcls(
-                    'flex',
-                    'justify-start',
-                    'items-center',
-                    'gap-3',
-                    'p-1.5',
-                    'pl-3',
-                    'text-sm',
-                    'transition-colors',
-                    'duration-100',
-                    'text-tint-strong/7',
-                    'rounded-md',
-                    'straight-corners:rounded-none',
-                    'before:content-none',
-                    'font-normal',
-                    'hover:bg-tint',
-                    'hover:text-tint-strong'
-                )}
+                classNames={['PageLinkItemStyles']}
                 insights={{
                     type: 'link_click',
                     link: {
@@ -55,9 +38,9 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Git
                         'shrink-0',
                         'text-current',
                         'transition-colors',
-                        '[&>path]:transition-[opacity]',
-                        '[&>path]:[opacity:0.40]',
-                        'group-hover:[&>path]:[opacity:1]'
+                        '[&>path]:transition-opacity',
+                        '[&>path]:opacity-[0.4]',
+                        'group-hover:[&>path]:opacity-11'
                     )}
                 />
             </Link>
diff --git a/packages/gitbook/src/components/TableOfContents/PagesList.tsx b/packages/gitbook/src/components/TableOfContents/PagesList.tsx
index ff92f269b7..ccd7579ec3 100644
--- a/packages/gitbook/src/components/TableOfContents/PagesList.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PagesList.tsx
@@ -1,8 +1,9 @@
-import { type RevisionPage, RevisionPageType } from '@gitbook/api';
+import type { RevisionPage } from '@gitbook/api';
 import type { GitBookSiteContext } from '@v2/lib/context';
 
 import { type ClassValue, tcls } from '@/lib/tailwind';
 
+import assertNever from 'assert-never';
 import { PageDocumentItem } from './PageDocumentItem';
 import { PageGroupItem } from './PageGroupItem';
 import { PageLinkItem } from './PageLinkItem';
@@ -16,41 +17,45 @@ export function PagesList(props: {
     const { rootPages, pages, context, style } = props;
 
     return (
-        <ul className={tcls('flex', 'flex-col', 'gap-y-0.5', style)}>
+        <ul className={tcls('flex flex-col gap-y-0.5', style)}>
             {pages.map((page) => {
-                if (page.type === RevisionPageType.Computed) {
+                if (page.type === 'computed') {
                     throw new Error(
                         'Unexpected computed page, it should have been computed in the API'
                     );
                 }
 
-                if (page.type === RevisionPageType.Link) {
-                    return <PageLinkItem key={page.id} page={page} context={context} />;
-                }
-
                 if (page.hidden) {
                     return null;
                 }
 
-                if (page.type === RevisionPageType.Group) {
-                    return (
-                        <PageGroupItem
-                            key={page.id}
-                            rootPages={rootPages}
-                            page={page}
-                            context={context}
-                        />
-                    );
+                switch (page.type) {
+                    case 'document':
+                        return (
+                            <PageDocumentItem
+                                key={page.id}
+                                rootPages={rootPages}
+                                page={page}
+                                context={context}
+                            />
+                        );
+
+                    case 'link':
+                        return <PageLinkItem key={page.id} page={page} context={context} />;
+
+                    case 'group':
+                        return (
+                            <PageGroupItem
+                                key={page.id}
+                                rootPages={rootPages}
+                                page={page}
+                                context={context}
+                            />
+                        );
+
+                    default:
+                        assertNever(page);
                 }
-
-                return (
-                    <PageDocumentItem
-                        key={page.id}
-                        rootPages={rootPages}
-                        page={page}
-                        context={context}
-                    />
-                );
             })}
         </ul>
     );
diff --git a/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx b/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx
index aef502c446..266f3c1e2b 100644
--- a/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx
+++ b/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx
@@ -13,7 +13,7 @@ export function TOCPageIcon({ page }: { page: RevisionPage }) {
             page={page}
             style={tcls(
                 'text-base',
-                'text-tint-strong/6',
+                '[.toclink_&]:text-tint-strong/6',
                 'group-aria-current-page/toclink:text-primary-subtle',
                 'contrast-more:group-aria-current-page/toclink:text-primary',
 
diff --git a/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx b/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx
index 9e9332ea1c..492dc9022d 100644
--- a/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx
+++ b/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx
@@ -1,40 +1,64 @@
 'use client';
 
-import React from 'react';
+import React, {
+    useCallback,
+    useEffect,
+    useMemo,
+    useRef,
+    type ComponentPropsWithoutRef,
+} from 'react';
+import { assert } from 'ts-essentials';
 
-import { type ClassValue, tcls } from '@/lib/tailwind';
+interface TOCScrollContainerContextType {
+    onContainerMount: (listener: (element: HTMLDivElement) => void) => () => void;
+}
 
-const TOCScrollContainerRefContext = React.createContext<React.RefObject<HTMLDivElement> | null>(
-    null
-);
+const TOCScrollContainerContext = React.createContext<TOCScrollContainerContextType | null>(null);
 
-function useTOCScrollContainerRefContext() {
-    const ctx = React.useContext(TOCScrollContainerRefContext);
-    if (!ctx) {
-        throw new Error('Context `TOCScrollContainerRefContext` must be used within Provider');
-    }
+function useTOCScrollContainerContext() {
+    const ctx = React.useContext(TOCScrollContainerContext);
+    assert(ctx);
     return ctx;
 }
 
-export function TOCScrollContainer(props: {
-    children: React.ReactNode;
-    className?: ClassValue;
-    style?: React.CSSProperties;
-}) {
-    const { children, className, style } = props;
-    const scrollContainerRef = React.createRef<HTMLDivElement>();
+/**
+ * Table of contents scroll container.
+ */
+export function TOCScrollContainer(props: ComponentPropsWithoutRef<'div'>) {
+    const ref = useRef<HTMLDivElement>(null);
+    const listeners = useRef<((element: HTMLDivElement) => void)[]>([]);
+    const onContainerMount: TOCScrollContainerContextType['onContainerMount'] = useCallback(
+        (listener) => {
+            if (ref.current) {
+                listener(ref.current);
+                return () => {};
+            }
+            listeners.current.push(listener);
+            return () => {
+                listeners.current = listeners.current.filter((l) => l !== listener);
+            };
+        },
+        []
+    );
+    const value: TOCScrollContainerContextType = useMemo(
+        () => ({ onContainerMount }),
+        [onContainerMount]
+    );
+    useEffect(() => {
+        const element = ref.current;
+        if (!element) {
+            return;
+        }
+        listeners.current.forEach((listener) => listener(element));
+        return () => {
+            listeners.current = [];
+        };
+    }, []);
 
     return (
-        <TOCScrollContainerRefContext.Provider value={scrollContainerRef}>
-            <div
-                ref={scrollContainerRef}
-                data-testid="toc-scroll-container"
-                className={tcls(className)}
-                style={style}
-            >
-                {children}
-            </div>
-        </TOCScrollContainerRefContext.Provider>
+        <TOCScrollContainerContext.Provider value={value}>
+            <div ref={ref} data-testid="toc-scroll-container" {...props} />
+        </TOCScrollContainerContext.Provider>
     );
 }
 
@@ -42,39 +66,31 @@ export function TOCScrollContainer(props: {
 const TOC_ITEM_OFFSET = 100;
 
 /**
- * Scrolls the table of contents container to the page item when it becomes active
+ * Scrolls the table of contents container to the page item when it's initially active.
  */
 export function useScrollToActiveTOCItem(props: {
+    anchorRef: React.RefObject<HTMLAnchorElement>;
     isActive: boolean;
-    linkRef: React.RefObject<HTMLAnchorElement>;
 }) {
-    const { isActive, linkRef } = props;
-    const scrollContainerRef = useTOCScrollContainerRefContext();
-    const isScrolled = React.useRef(false);
-    React.useLayoutEffect(() => {
-        if (!isActive) {
-            isScrolled.current = false;
-            return;
-        }
-        if (isScrolled.current) {
-            return;
-        }
-        const tocItem = linkRef.current;
-        const tocContainer = scrollContainerRef.current;
-        if (!tocItem || !tocContainer || !isOutOfView(tocItem, tocContainer)) {
-            return;
+    const { isActive, anchorRef } = props;
+    const isInitialActiveRef = useRef(isActive);
+    const { onContainerMount } = useTOCScrollContainerContext();
+    useEffect(() => {
+        const anchor = anchorRef.current;
+        if (isInitialActiveRef.current && anchor) {
+            return onContainerMount((container) => {
+                if (isOutOfView(anchor, container)) {
+                    container.scrollTo({ top: anchor.offsetTop - TOC_ITEM_OFFSET });
+                }
+            });
         }
-        tocContainer?.scrollTo({
-            top: tocItem.offsetTop - TOC_ITEM_OFFSET,
-        });
-        isScrolled.current = true;
-    }, [isActive, linkRef, scrollContainerRef]);
+    }, [onContainerMount, anchorRef]);
 }
 
-function isOutOfView(tocItem: HTMLElement, tocContainer: HTMLElement) {
-    const tocItemTop = tocItem.offsetTop;
-    const containerTop = tocContainer.scrollTop;
-    const containerBottom = containerTop + tocContainer.clientHeight;
+function isOutOfView(element: HTMLElement, container: HTMLElement) {
+    const tocItemTop = element.offsetTop;
+    const containerTop = container.scrollTop;
+    const containerBottom = containerTop + container.clientHeight;
     return (
         tocItemTop < containerTop + TOC_ITEM_OFFSET ||
         tocItemTop > containerBottom - TOC_ITEM_OFFSET
diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
index 9f9c0d85da..7ca7a2c0c0 100644
--- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
+++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx
@@ -6,6 +6,7 @@ import { tcls } from '@/lib/tailwind';
 
 import { PagesList } from './PagesList';
 import { TOCScrollContainer } from './TOCScroller';
+import { TableOfContentsScript } from './TableOfContentsScript';
 import { Trademark } from './Trademark';
 
 export function TableOfContents(props: {
@@ -17,111 +18,110 @@ export function TableOfContents(props: {
     const { space, customization, pages } = context;
 
     return (
-        <aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
-            data-testid="table-of-contents"
-            className={tcls(
-                'group',
-                'text-sm',
-
-                'grow-0',
-                'shrink-0',
-                'basis-full',
-                'lg:basis-72',
-                'page-no-toc:lg:basis-56',
-
-                'relative',
-                'z-[1]',
-                'lg:sticky',
-                // Without header
-                'lg:top-0',
-                'lg:h-screen',
-
-                // With header
-                'site-header:lg:top-16',
-                'site-header:lg:h-[calc(100vh_-_4rem)]',
-
-                // With header and sections
-                'site-header-sections:lg:top-[6.75rem]',
-                'site-header-sections:lg:h-[calc(100vh_-_6.75rem)]',
-
-                'pt-6',
-                'pb-4',
-                'sidebar-filled:lg:pr-6',
-                'page-no-toc:lg:pr-0',
+        <>
+            <aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
+                data-testid="table-of-contents"
+                id="table-of-contents"
+                className={tcls(
+                    'group',
+                    'text-sm',
 
-                'hidden',
-                'navigation-open:!flex',
-                'lg:flex',
-                'page-no-toc:lg:hidden',
-                'page-no-toc:xl:flex',
-                'site-header-none:page-no-toc:lg:flex',
-                'flex-col',
-                'gap-4',
+                    'grow-0',
+                    'shrink-0',
+                    'basis-full',
+                    'lg:basis-72',
+                    'page-no-toc:lg:basis-56',
 
-                'navigation-open:border-b',
-                'border-tint-subtle'
-            )}
-        >
-            {header && header}
-            <div // The actual sidebar, either shown with a filled bg or transparent.
-                className={tcls(
-                    'lg:-ms-5',
-                    'overflow-hidden',
                     'relative',
-
-                    'flex',
+                    'z-[1]',
+                    'lg:sticky',
+                    'lg:mr-12',
+
+                    // Server-side static positioning
+                    'lg:top-0',
+                    'lg:h-screen',
+                    'announcement:lg:h-[calc(100vh-4.25rem)]',
+
+                    'site-header:lg:top-16',
+                    'site-header:lg:h-[calc(100vh-4rem)]',
+                    'announcement:site-header:lg:h-[calc(100vh-4rem-4.25rem)]',
+
+                    'site-header-sections:lg:top-[6.75rem]',
+                    'site-header-sections:lg:h-[calc(100vh-6.75rem)]',
+                    'announcement:site-header-sections:lg:h-[calc(100vh-6.75rem-4.25rem)]',
+
+                    // Client-side dynamic positioning (CSS vars applied by script)
+                    '[html[style*="--toc-top-offset"]_&]:lg:!top-[var(--toc-top-offset)]',
+                    '[html[style*="--toc-height"]_&]:lg:!h-[var(--toc-height)]',
+
+                    'pt-6',
+                    'pb-4',
+                    'sidebar-filled:lg:pr-6',
+                    'page-no-toc:lg:pr-0',
+
+                    'hidden',
+                    'navigation-open:!flex',
+                    'lg:flex',
+                    'page-no-toc:lg:hidden',
+                    'page-no-toc:xl:flex',
+                    'site-header-none:page-no-toc:lg:flex',
                     'flex-col',
-                    'flex-grow',
-
-                    'sidebar-filled:bg-tint-subtle',
-                    'theme-muted:bg-tint-subtle',
-                    '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
-                    '[html.sidebar-filled.theme-muted_&]:bg-tint-base',
-                    '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
-                    'page-no-toc:!bg-transparent',
+                    'gap-4',
 
-                    'sidebar-filled:rounded-xl',
-                    'straight-corners:rounded-none'
+                    'navigation-open:border-b',
+                    'border-tint-subtle'
                 )}
             >
-                {innerHeader && <div className={tcls('px-5 *:my-4')}>{innerHeader}</div>}
-                <TOCScrollContainer // The scrollview inside the sidebar
+                {header && header}
+                <div // The actual sidebar, either shown with a filled bg or transparent.
                     className={tcls(
-                        'flex',
-                        'flex-grow',
-                        'flex-col',
-
-                        'p-2',
-                        customization.trademark.enabled && 'lg:pb-20',
-
-                        'overflow-y-auto',
-                        'lg:gutter-stable',
-                        '[&::-webkit-scrollbar]:bg-transparent',
-                        '[&::-webkit-scrollbar-thumb]:bg-transparent',
-                        'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle',
-                        'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7',
-                        'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8'
+                        'lg:-ms-5',
+                        'relative flex flex-grow flex-col overflow-hidden border-tint-subtle',
+
+                        'sidebar-filled:bg-tint-subtle',
+                        'theme-muted:bg-tint-subtle',
+                        '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
+                        '[html.sidebar-filled.theme-muted_&]:bg-tint-base',
+                        '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
+                        '[html.sidebar-filled.theme-gradient_&]:border',
+                        'page-no-toc:!bg-transparent',
+                        'page-no-toc:!border-none',
+
+                        'sidebar-filled:rounded-xl',
+                        'straight-corners:rounded-none',
+                        '[html.sidebar-filled.circular-corners_&]:page-has-toc:rounded-3xl'
                     )}
                 >
-                    <PagesList
-                        rootPages={pages}
-                        pages={pages}
-                        context={context}
-                        style={tcls(
-                            'page-no-toc:hidden',
-                            'sidebar-list-line:border-l',
-                            'border-tint-subtle'
+                    {innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>}
+                    <TOCScrollContainer // The scrollview inside the sidebar
+                        className={tcls(
+                            'flex flex-grow flex-col p-2',
+                            customization.trademark.enabled && 'lg:pb-20',
+                            'lg:gutter-stable overflow-y-auto',
+                            '[&::-webkit-scrollbar]:bg-transparent',
+                            '[&::-webkit-scrollbar-thumb]:bg-transparent',
+                            'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle',
+                            'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7',
+                            'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8'
                         )}
-                    />
-                    {customization.trademark.enabled ? (
-                        <Trademark
-                            space={space}
-                            customization={customization}
-                            placement={SiteInsightsTrademarkPlacement.Sidebar}
+                    >
+                        <PagesList
+                            rootPages={pages}
+                            pages={pages}
+                            context={context}
+                            style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
                         />
-                    ) : null}
-                </TOCScrollContainer>
-            </div>
-        </aside>
+                        {customization.trademark.enabled ? (
+                            <Trademark
+                                space={space}
+                                customization={customization}
+                                placement={SiteInsightsTrademarkPlacement.Sidebar}
+                            />
+                        ) : null}
+                    </TOCScrollContainer>
+                </div>
+            </aside>
+            <TableOfContentsScript />
+        </>
     );
 }
diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx
new file mode 100644
index 0000000000..a4e6bea18c
--- /dev/null
+++ b/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx
@@ -0,0 +1,73 @@
+'use client';
+
+import { useEffect } from 'react';
+
+/**
+ * Adjusts TableOfContents height based on visible elements
+ */
+export function TableOfContentsScript() {
+    useEffect(() => {
+        const root = document.documentElement;
+
+        // Calculate and set TOC dimensions
+        const updateTocLayout = () => {
+            // Get key elements
+            const header = document.getElementById('site-header');
+            const banner = document.getElementById('announcement-banner');
+            const footer = document.getElementById('site-footer');
+
+            // Set sticky top position based on header
+            const headerHeight = header?.offsetHeight ?? 0;
+            root.style.setProperty('--toc-top-offset', `${headerHeight}px`);
+
+            // Start with full viewport height minus header
+            let height = window.innerHeight - headerHeight;
+
+            // Subtract visible banner (if any)
+            if (banner && window.getComputedStyle(banner).display !== 'none') {
+                const bannerRect = banner.getBoundingClientRect();
+                if (bannerRect.height > 0 && bannerRect.bottom > 0) {
+                    height -= Math.min(bannerRect.height, bannerRect.bottom);
+                }
+            }
+
+            // Subtract visible footer (if any)
+            if (footer) {
+                const footerRect = footer.getBoundingClientRect();
+                if (footerRect.top < window.innerHeight) {
+                    height -= Math.min(footerRect.height, window.innerHeight - footerRect.top);
+                }
+            }
+
+            // Update height
+            root.style.setProperty('--toc-height', `${height}px`);
+        };
+
+        // Initial update
+        updateTocLayout();
+
+        // Let the browser handle scroll throttling naturally
+        window.addEventListener('scroll', updateTocLayout, { passive: true });
+        window.addEventListener('resize', updateTocLayout, { passive: true });
+
+        // Use MutationObserver for DOM changes
+        const observer = new MutationObserver(() => {
+            requestAnimationFrame(updateTocLayout);
+        });
+
+        // Only observe what matters
+        observer.observe(document.documentElement, {
+            subtree: true,
+            attributes: true,
+            attributeFilter: ['style', 'class'],
+        });
+
+        return () => {
+            observer.disconnect();
+            window.removeEventListener('scroll', updateTocLayout);
+            window.removeEventListener('resize', updateTocLayout);
+        };
+    }, []);
+
+    return null;
+}
diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
index aa4700482b..681df69080 100644
--- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx
@@ -2,12 +2,12 @@
 
 import { Icon } from '@gitbook/icons';
 import { motion } from 'framer-motion';
-import React from 'react';
+import React, { useRef } from 'react';
 
 import { tcls } from '@/lib/tailwind';
 
-import { useCurrentPagePath, useToggleAnimation } from '../hooks';
-import { Link, type LinkInsightsProps } from '../primitives';
+import { useCurrentPagePath } from '../hooks';
+import { Link, type LinkInsightsProps, type LinkProps } from '../primitives';
 import { useScrollToActiveTOCItem } from './TOCScroller';
 
 /**
@@ -24,128 +24,158 @@ export function ToggleableLinkItem(
     const { href, children, descendants, pathname, insights } = props;
 
     const currentPagePath = useCurrentPagePath();
-
     const isActive = currentPagePath === pathname;
-    const hasDescendants = !!descendants;
-    const hasActiveDescendant =
-        hasDescendants && (isActive || currentPagePath.startsWith(`${pathname}/`));
 
-    const [isVisible, setIsVisible] = React.useState(hasActiveDescendant);
+    if (!descendants) {
+        return (
+            <LinkItem href={href} insights={insights} isActive={isActive}>
+                {children}
+            </LinkItem>
+        );
+    }
 
-    // Update the visibility of the children, if we are navigating to a descendant.
-    React.useEffect(() => {
-        if (!hasDescendants) {
-            return;
-        }
+    return (
+        <DescendantsRenderer
+            descendants={descendants}
+            defaultIsOpen={isActive || currentPagePath.startsWith(`${pathname}/`)}
+        >
+            {({ descendants, toggler }) => (
+                <>
+                    <LinkItem href={href} insights={insights} isActive={isActive}>
+                        {children}
+                        {toggler}
+                    </LinkItem>
+                    {descendants}
+                </>
+            )}
+        </DescendantsRenderer>
+    );
+}
 
-        setIsVisible((prev) => prev || hasActiveDescendant);
-    }, [hasActiveDescendant, hasDescendants]);
+function LinkItem(
+    props: Pick<LinkProps, 'href' | 'insights' | 'children'> & {
+        isActive: boolean;
+    }
+) {
+    const { isActive, href, insights, children } = props;
+    const anchorRef = useRef<HTMLAnchorElement>(null);
+    useScrollToActiveTOCItem({ anchorRef, isActive });
+    return (
+        <Link
+            ref={anchorRef}
+            href={href}
+            insights={insights}
+            aria-current={isActive ? 'page' : undefined}
+            classNames={[
+                'ToggleableLinkItemStyles',
+                ...(isActive ? ['ToggleableLinkItemActiveStyles' as const] : []),
+            ]}
+        >
+            {children}
+        </Link>
+    );
+}
 
-    const { show, hide, scope } = useToggleAnimation({ hasDescendants, isVisible });
+function DescendantsRenderer(props: {
+    defaultIsOpen: boolean;
+    descendants: React.ReactNode;
+    children: (renderProps: {
+        descendants: React.ReactNode;
+        toggler: React.ReactNode;
+    }) => React.ReactNode;
+}) {
+    const { defaultIsOpen, children, descendants } = props;
+    const [isOpen, setIsOpen] = React.useState(defaultIsOpen);
 
-    const linkRef = React.createRef<HTMLAnchorElement>();
-    useScrollToActiveTOCItem({ linkRef, isActive });
+    // Update the visibility of the children if one of the descendants becomes active.
+    React.useEffect(() => {
+        if (defaultIsOpen) {
+            setIsOpen(defaultIsOpen);
+        }
+    }, [defaultIsOpen]);
+
+    return children({
+        toggler: (
+            <Toggler
+                isLinkActive={isOpen}
+                isOpen={isOpen}
+                onToggle={() => {
+                    setIsOpen((prev) => !prev);
+                }}
+            />
+        ),
+        descendants: <Descendants isVisible={isOpen}>{descendants}</Descendants>,
+    });
+}
 
+function Toggler(props: {
+    isLinkActive: boolean;
+    isOpen: boolean;
+    onToggle: () => void;
+}) {
+    const { isLinkActive, isOpen, onToggle } = props;
     return (
-        <>
-            <Link
-                ref={linkRef}
-                href={href}
-                insights={insights}
-                {...(isActive ? { 'aria-current': 'page' } : {})}
+        <span
+            className={tcls(
+                'group',
+                'relative',
+                'rounded-full',
+                'straight-corners:rounded-sm',
+                'w-5',
+                'h-5',
+                'after:grid-area-1-1',
+                'after:absolute',
+                'after:-top-1',
+                'after:grid',
+                'after:-left-1',
+                'after:w-7',
+                'after:h-7',
+                'hover:bg-tint-active',
+                'hover:text-current',
+                isLinkActive && 'hover:bg-tint-hover'
+            )}
+            onClick={(event) => {
+                event.preventDefault();
+                event.stopPropagation();
+                onToggle();
+            }}
+        >
+            <Icon
+                icon="chevron-right"
                 className={tcls(
-                    'group/toclink relative transition-colors',
-                    'flex flex-row justify-between',
-                    'rounded-md straight-corners:rounded-none p-1.5 pl-3',
-                    'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
-                    'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12',
-                    'before:contents[] before:-left-px before:absolute before:inset-y-0',
-                    'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px',
-
-                    isActive && [
-                        'font-semibold',
-                        'sidebar-list-line:before:w-0.5',
-
-                        'before:bg-primary-solid',
-                        'text-primary-subtle',
-                        'contrast-more:text-primary',
-
-                        'sidebar-list-pill:bg-primary',
-                        '[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover',
-                        '[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover',
-                        '[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary',
-                        '[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary',
-
-                        'hover:bg-primary-hover',
-                        'hover:text-primary',
-                        'hover:before:bg-primary-solid-hover',
-                        'sidebar-list-pill:hover:bg-primary-hover',
-
-                        'contrast-more:text-primary',
-                        'contrast-more:hover:text-primary-strong',
-                        'contrast-more:bg-primary',
-                        'contrast-more:ring-1',
-                        'contrast-more:ring-primary',
-                        'contrast-more:hover:ring-primary-hover',
-                    ]
+                    'm-1 grid size-3 flex-shrink-0 text-current opacity-6 transition',
+                    'group-hover:opacity-11 contrast-more:opacity-11',
+                    isOpen ? 'rotate-90' : 'rotate-0'
                 )}
-            >
-                {children}
-                {hasDescendants ? (
-                    <span
-                        className={tcls(
-                            'group',
-                            'relative',
-                            'rounded-full',
-                            'straight-corners:rounded-sm',
-                            'w-5',
-                            'h-5',
-                            'after:grid-area-1-1',
-                            'after:absolute',
-                            'after:-top-1',
-                            'after:grid',
-                            'after:-left-1',
-                            'after:w-7',
-                            'after:h-7',
-                            'hover:bg-tint-active',
-                            'hover:text-current',
-                            isActive ? ['hover:bg-tint-hover'] : []
-                        )}
-                        onClick={(event) => {
-                            event.preventDefault();
-                            event.stopPropagation();
-                            setIsVisible((prev) => !prev);
-                        }}
-                    >
-                        <Icon
-                            icon="chevron-right"
-                            className={tcls(
-                                'grid',
-                                'flex-shrink-0',
-                                'size-3',
-                                'm-1',
-                                'transition-[opacity]',
-                                'text-current',
-                                'transition-transform',
-                                'opacity-6',
-                                'group-hover:opacity-11',
-                                'contrast-more:opacity-11',
+            />
+        </span>
+    );
+}
 
-                                isVisible ? ['rotate-90'] : ['rotate-0']
-                            )}
-                        />
-                    </span>
-                ) : null}
-            </Link>
-            {hasDescendants ? (
-                <motion.div
-                    ref={scope}
-                    className={tcls(isVisible ? null : '[&_ul>li]:opacity-1')}
-                    initial={isVisible ? show : hide}
-                >
-                    {descendants}
-                </motion.div>
-            ) : null}
-        </>
+const show = {
+    opacity: 1,
+    height: 'auto',
+};
+const hide = {
+    opacity: 0,
+    height: 0,
+    transitionEnd: {
+        display: 'none',
+    },
+};
+
+function Descendants(props: {
+    isVisible: boolean;
+    children: React.ReactNode;
+}) {
+    const { isVisible, children } = props;
+    return (
+        <motion.div
+            className="overflow-hidden"
+            animate={isVisible ? show : hide}
+            initial={isVisible ? show : hide}
+        >
+            {children}
+        </motion.div>
     );
 }
diff --git a/packages/gitbook/src/components/TableOfContents/Trademark.tsx b/packages/gitbook/src/components/TableOfContents/Trademark.tsx
index 19f5aed660..aa2d37c0ac 100644
--- a/packages/gitbook/src/components/TableOfContents/Trademark.tsx
+++ b/packages/gitbook/src/components/TableOfContents/Trademark.tsx
@@ -102,6 +102,7 @@ export function TrademarkLink(props: {
 
                 'rounded-lg',
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-2xl',
 
                 'hover:bg-tint',
                 'hover:text-tint-strong',
diff --git a/packages/gitbook/src/components/TableOfContents/index.ts b/packages/gitbook/src/components/TableOfContents/index.ts
index 715a3a239e..6eeff92697 100644
--- a/packages/gitbook/src/components/TableOfContents/index.ts
+++ b/packages/gitbook/src/components/TableOfContents/index.ts
@@ -1 +1,4 @@
-export * from './TableOfContents';
+export { TableOfContents } from './TableOfContents';
+export { PagesList } from './PagesList';
+export { TOCScrollContainer } from './TOCScroller';
+export { Trademark } from './Trademark';
diff --git a/packages/gitbook/src/components/TableOfContents/styles.ts b/packages/gitbook/src/components/TableOfContents/styles.ts
new file mode 100644
index 0000000000..3ceb636941
--- /dev/null
+++ b/packages/gitbook/src/components/TableOfContents/styles.ts
@@ -0,0 +1,56 @@
+export const PageLinkItemStyles = [
+    'flex',
+    'justify-start',
+    'items-center',
+    'gap-3',
+    'p-1.5',
+    'pl-3',
+    'text-sm',
+    'transition-colors',
+    'duration-100',
+    'text-tint-strong/7',
+    'rounded-md',
+    'straight-corners:rounded-none',
+    'circular-corners:rounded-xl',
+    'before:content-none',
+    'font-normal',
+    'hover:bg-tint',
+    'hover:text-tint-strong',
+];
+
+export const ToggleableLinkItemStyles = [
+    'group/toclink toclink relative transition-colors',
+    'flex flex-row justify-between',
+    'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
+    'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
+    'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12',
+    'before:contents[] before:-left-px before:absolute before:inset-y-0',
+    'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px',
+];
+
+export const ToggleableLinkItemActiveStyles = [
+    'font-semibold',
+    'sidebar-list-line:before:w-0.5',
+
+    'before:bg-primary-solid',
+    'text-primary-subtle',
+    'contrast-more:text-primary',
+
+    'sidebar-list-pill:bg-primary',
+    '[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover',
+    '[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover',
+    '[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary',
+    '[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary',
+
+    'hover:bg-primary-hover',
+    'hover:text-primary',
+    'hover:before:bg-primary-solid-hover',
+    'sidebar-list-pill:hover:bg-primary-hover',
+
+    'contrast-more:text-primary',
+    'contrast-more:hover:text-primary-strong',
+    'contrast-more:bg-primary',
+    'contrast-more:ring-1',
+    'contrast-more:ring-primary',
+    'contrast-more:hover:ring-primary-hover',
+];
diff --git a/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx b/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx
index af943dcbce..93f15e47c4 100644
--- a/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx
+++ b/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx
@@ -69,6 +69,7 @@ function ThemeButton(props: {
                 'p-2',
                 'rounded',
                 'straight-corners:rounded-none',
+                'circular-corners:rounded-full',
                 'transition-all',
                 'text-tint',
                 'contrast-more:text-tint-strong',
diff --git a/packages/gitbook/src/components/hooks/useCurrentPagePath.ts b/packages/gitbook/src/components/hooks/useCurrentPagePath.ts
index f1f719ddd7..23943e00b0 100644
--- a/packages/gitbook/src/components/hooks/useCurrentPagePath.ts
+++ b/packages/gitbook/src/components/hooks/useCurrentPagePath.ts
@@ -3,6 +3,7 @@
 import { useParams, useSelectedLayoutSegment } from 'next/navigation';
 
 import { removeLeadingSlash } from '@/lib/paths';
+import { useMemo } from 'react';
 
 /**
  * Return the page of the current page being rendered.
@@ -14,9 +15,11 @@ export function useCurrentPagePath() {
     // For V1, we use the selected layout segment.
     const rawActiveSegment = useSelectedLayoutSegment() ?? '';
 
-    if (typeof params.pagePath === 'string') {
-        return removeLeadingSlash(decodeURIComponent(params.pagePath));
-    }
+    return useMemo(() => {
+        if (typeof params.pagePath === 'string') {
+            return removeLeadingSlash(decodeURIComponent(params.pagePath));
+        }
 
-    return decodeURIComponent(rawActiveSegment);
+        return decodeURIComponent(rawActiveSegment);
+    }, [params.pagePath, rawActiveSegment]);
 }
diff --git a/packages/gitbook/src/components/hooks/useToggleAnimation.ts b/packages/gitbook/src/components/hooks/useToggleAnimation.ts
index c028155142..3c9c02fad7 100644
--- a/packages/gitbook/src/components/hooks/useToggleAnimation.ts
+++ b/packages/gitbook/src/components/hooks/useToggleAnimation.ts
@@ -1,7 +1,7 @@
-import { stagger, useAnimate } from 'framer-motion';
-import React from 'react';
+'use client';
 
-import { useIsMounted } from '.';
+import { stagger, useAnimate } from 'framer-motion';
+import React, { useLayoutEffect } from 'react';
 
 const show = {
     opacity: 1,
@@ -19,44 +19,35 @@ const hide = {
 
 const staggerMenuItems = stagger(0.02, { ease: (p) => p ** 2 });
 
-export function useToggleAnimation({
-    hasDescendants,
-    isVisible,
-}: {
-    hasDescendants: boolean;
-    isVisible: boolean;
-}) {
-    const isMounted = useIsMounted();
+export function useToggleAnimation(isVisible: boolean) {
     const [scope, animate] = useAnimate();
+    const previousIsVisibleRef = React.useRef<boolean | undefined>(undefined);
+    useLayoutEffect(() => {
+        previousIsVisibleRef.current = isVisible;
+    });
+    const previousIsVisible = previousIsVisibleRef.current;
 
     // Animate the visibility of the children
     // only after the initial state.
     React.useEffect(() => {
-        if (!isMounted || !hasDescendants) {
+        if (previousIsVisible === undefined || previousIsVisible === isVisible) {
             return;
         }
+
         try {
-            animate(scope.current, isVisible ? show : hide, {
-                duration: 0.1,
-            });
+            animate(scope.current, isVisible ? show : hide, { duration: 0.1 });
 
             const selector = '& > ul > li';
-            if (isVisible)
-                animate(
-                    selector,
-                    { opacity: 1 },
-                    {
-                        delay: staggerMenuItems,
-                    }
-                );
-            else {
+            if (isVisible) {
+                animate(selector, { opacity: 1 }, { delay: staggerMenuItems });
+            } else {
                 animate(selector, { opacity: 0 });
             }
         } catch (error) {
             // The selector can crash in some browsers, we ignore it as the animation is not critical.
             console.error(error);
         }
-    }, [isVisible, isMounted, hasDescendants, animate, scope]);
+    }, [previousIsVisible, isVisible, animate, scope]);
 
     return { show, hide, scope };
 }
diff --git a/packages/gitbook/src/components/layout.ts b/packages/gitbook/src/components/layout.ts
index 250d3366ab..6a9b39a982 100644
--- a/packages/gitbook/src/components/layout.ts
+++ b/packages/gitbook/src/components/layout.ts
@@ -14,7 +14,7 @@ export const CONTAINER_STYLE: ClassValue = [
     'md:px-8',
     'max-w-screen-2xl',
     'mx-auto',
-    'page-full-width:max-w-full',
+    // 'site-full-width:max-w-full',
 ];
 
 /**
diff --git a/packages/gitbook/src/components/primitives/Button.tsx b/packages/gitbook/src/components/primitives/Button.tsx
index 01d8880077..4ccc10b6b7 100644
--- a/packages/gitbook/src/components/primitives/Button.tsx
+++ b/packages/gitbook/src/components/primitives/Button.tsx
@@ -6,6 +6,7 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
 
 import { Icon, type IconName } from '@gitbook/icons';
 import { Link, type LinkInsightsProps } from './Link';
+import { useClassnames } from './StyleProvider';
 
 type ButtonProps = {
     href?: string;
@@ -18,7 +19,7 @@ type ButtonProps = {
 } & LinkInsightsProps &
     HTMLAttributes<HTMLElement>;
 
-const variantClasses = {
+export const variantClasses = {
     primary: [
         'bg-primary-solid',
         'text-contrast-primary-solid',
@@ -40,8 +41,10 @@ const variantClasses = {
     ],
     secondary: [
         'bg-tint',
+        'depth-flat:bg-transparent',
         'text-tint',
         'hover:bg-tint-hover',
+        'depth-flat:hover:bg-tint-hover',
         'hover:text-primary',
         'contrast-more:bg-tint-subtle',
     ],
@@ -60,53 +63,22 @@ export function Button({
     ...rest
 }: ButtonProps & { target?: HTMLAttributeAnchorTarget }) {
     const sizes = {
-        default: ['text-base', 'px-4', 'py-2'],
-        medium: ['text-sm', 'px-3', 'py-1.5'],
+        default: ['text-base', 'font-semibold', 'px-5', 'py-2', 'circular-corners:px-6'],
+        medium: ['text-sm', 'px-3.5', 'py-1.5', 'circular-corners:px-4'],
         small: ['text-xs', 'py-2', iconOnly ? 'px-2' : 'px-3'],
     };
 
     const sizeClasses = sizes[size] || sizes.default;
 
-    const domClassName = tcls(
-        'button',
-        'inline-flex',
-        'items-center',
-        'gap-2',
-        'rounded-md',
-        'straight-corners:rounded-none',
-        // 'place-self-start',
-
-        'ring-1',
-        'ring-tint',
-        'hover:ring-tint-hover',
-
-        'shadow-sm',
-        'shadow-tint',
-        'dark:shadow-tint-1',
-        'hover:shadow-md',
-        'active:shadow-none',
-
-        'contrast-more:ring-tint-12',
-        'contrast-more:hover:ring-2',
-        'contrast-more:hover:ring-tint-12',
-
-        'hover:scale-105',
-        'active:scale-100',
-        'transition-all',
-
-        'grow-0',
-        'shrink-0',
-        'truncate',
-        variantClasses[variant],
-        sizeClasses,
-        className
-    );
+    const domClassName = tcls(variantClasses[variant], sizeClasses, className);
+    const buttonOnlyClassNames = useClassnames(['ButtonStyles']);
 
     if (href) {
         return (
             <Link
                 href={href}
                 className={domClassName}
+                classNames={['ButtonStyles']}
                 insights={insights}
                 aria-label={label}
                 target={target}
@@ -119,7 +91,12 @@ export function Button({
     }
 
     return (
-        <button type="button" className={domClassName} aria-label={label} {...rest}>
+        <button
+            type="button"
+            className={tcls(domClassName, buttonOnlyClassNames)}
+            aria-label={label}
+            {...rest}
+        >
             {icon ? <Icon icon={icon} className={tcls('size-[1em]')} /> : null}
             {iconOnly ? null : label}
         </button>
diff --git a/packages/gitbook/src/components/primitives/Card.tsx b/packages/gitbook/src/components/primitives/Card.tsx
index bb40142f31..1195c78476 100644
--- a/packages/gitbook/src/components/primitives/Card.tsx
+++ b/packages/gitbook/src/components/primitives/Card.tsx
@@ -17,27 +17,7 @@ export async function Card(
     const { title, leadingIcon, href, preTitle, postTitle, style, insights } = props;
 
     return (
-        <Link
-            href={href}
-            className={tcls(
-                'group',
-                'flex',
-                'flex-row',
-                'justify-between',
-                'items-center',
-                'gap-4',
-                'ring-1',
-                'ring-tint-subtle',
-                'rounded',
-                'straight-corners:rounded-none',
-                'px-5',
-                'py-3',
-                'transition-shadow',
-                'hover:ring-primary-hover',
-                style
-            )}
-            insights={insights}
-        >
+        <Link href={href} className={tcls(style)} classNames={['CardStyles']} insights={insights}>
             {leadingIcon}
             <span className={tcls('flex', 'flex-col', 'flex-1')}>
                 {preTitle ? (
diff --git a/packages/gitbook/src/components/primitives/Link.tsx b/packages/gitbook/src/components/primitives/Link.tsx
index 5cb026d3a5..e15a01ca11 100644
--- a/packages/gitbook/src/components/primitives/Link.tsx
+++ b/packages/gitbook/src/components/primitives/Link.tsx
@@ -3,7 +3,9 @@
 import NextLink, { type LinkProps as NextLinkProps } from 'next/link';
 import React from 'react';
 
+import { tcls } from '@/lib/tailwind';
 import { type TrackEventInput, useTrackEvent } from '../Insights';
+import { type DesignTokenName, useClassnames } from './StyleProvider';
 
 // Props from Next, which includes NextLinkProps and all the things anchor elements support.
 type BaseLinkProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof NextLinkProps> &
@@ -24,6 +26,8 @@ export type LinkProps = Omit<BaseLinkProps, 'href'> &
     LinkInsightsProps & {
         /** Enforce href is passed as a string (not a URL). */
         href: string;
+        /** This is a temporary solution designed to reduce the number of tailwind class passed to the client */
+        classNames?: DesignTokenName[];
     };
 
 /**
@@ -34,8 +38,9 @@ export const Link = React.forwardRef(function Link(
     props: LinkProps,
     ref: React.Ref<HTMLAnchorElement>
 ) {
-    const { href, prefetch, children, insights, ...domProps } = props;
+    const { href, prefetch, children, insights, classNames, className, ...domProps } = props;
     const trackEvent = useTrackEvent();
+    const forwardedClassNames = useClassnames(classNames || []);
 
     // Use a real anchor tag for external links,s and a Next.js Link for internal links.
     // If we use a NextLink for external links, Nextjs won't rerender the top-level layouts.
@@ -62,19 +67,74 @@ export const Link = React.forwardRef(function Link(
     // as this will be rendered on the server and it could result in a mismatch.
     if (isExternalLink(href)) {
         return (
-            <a ref={ref} {...domProps} href={href} onClick={onClick}>
+            <a
+                ref={ref}
+                className={tcls(...forwardedClassNames, className)}
+                {...domProps}
+                href={href}
+                onClick={onClick}
+            >
                 {children}
             </a>
         );
     }
 
     return (
-        <NextLink ref={ref} href={href} prefetch={prefetch} {...domProps} onClick={onClick}>
+        <NextLink
+            ref={ref}
+            href={href}
+            prefetch={prefetch}
+            className={tcls(...forwardedClassNames, className)}
+            {...domProps}
+            onClick={onClick}
+        >
             {children}
         </NextLink>
     );
 });
 
+/**
+ * A box used to contain a link overlay.
+ * It is used to create a clickable area that can contain other elements.
+ */
+export const LinkBox = React.forwardRef(function LinkBox(
+    props: React.BaseHTMLAttributes<HTMLDivElement> & { classNames?: DesignTokenName[] },
+    ref: React.Ref<HTMLDivElement>
+) {
+    const { children, className, classNames, ...domProps } = props;
+    const forwardedClassNames = useClassnames(classNames || []);
+    return (
+        <div
+            ref={ref}
+            {...domProps}
+            className={tcls('elevate-link relative', className, forwardedClassNames)}
+        >
+            {children}
+        </div>
+    );
+});
+
+/**
+ * A link overlay that can be used to create a clickable area on top of other elements.
+ * It is used to create a link that covers the entire area of the element without encapsulating it in a link tag.
+ * This is useful to avoid nesting links inside links.
+ */
+export const LinkOverlay = React.forwardRef(function LinkOverlay(
+    props: LinkProps,
+    ref: React.Ref<HTMLAnchorElement>
+) {
+    const { children, className, ...domProps } = props;
+    return (
+        <Link
+            ref={ref}
+            {...domProps}
+            className={tcls('link-overlay absolute inset-0 z-10', className)}
+        >
+            {children}
+        </Link>
+    );
+});
+
 /**
  * Check if a link is external, compared to an origin.
  */
diff --git a/packages/gitbook/src/components/primitives/StyleProvider.tsx b/packages/gitbook/src/components/primitives/StyleProvider.tsx
new file mode 100644
index 0000000000..a1e1b1fe03
--- /dev/null
+++ b/packages/gitbook/src/components/primitives/StyleProvider.tsx
@@ -0,0 +1,32 @@
+'use client';
+import type { ClassValue } from '@/lib/tailwind';
+
+import { RecordCardStyles } from '../DocumentView/Table/styles';
+import {
+    PageLinkItemStyles,
+    ToggleableLinkItemActiveStyles,
+    ToggleableLinkItemStyles,
+} from '../TableOfContents/styles';
+import { ButtonStyles, CardStyles, LinkStyles } from './styles';
+
+const styles = {
+    LinkStyles,
+    CardStyles,
+    ButtonStyles,
+    RecordCardStyles,
+    PageLinkItemStyles,
+    ToggleableLinkItemStyles,
+    ToggleableLinkItemActiveStyles,
+};
+
+export type DesignTokenName = keyof typeof styles;
+
+/**
+ * Get the class names for the given design token names.
+ * TODO: remove this once we figure out a better solution. Likely with TW4.
+ * @param names The design token names to get class names for.
+ * @returns The class names for the given design token names.
+ */
+export function useClassnames(names: DesignTokenName[]): ClassValue[] {
+    return names.flatMap((name) => styles[name] || []);
+}
diff --git a/packages/gitbook/src/components/primitives/StyledLink.tsx b/packages/gitbook/src/components/primitives/StyledLink.tsx
index 3fb346f5f6..f3b391b67e 100644
--- a/packages/gitbook/src/components/primitives/StyledLink.tsx
+++ b/packages/gitbook/src/components/primitives/StyledLink.tsx
@@ -1,35 +1,18 @@
-import { type ClassValue, tcls } from '@/lib/tailwind';
+import type { ClassValue } from '@/lib/tailwind';
 
 import { Link, type LinkProps } from '../primitives/Link';
-
-export const linkStyles = [
-    'underline',
-    'decoration-[max(0.07em,1px)]', // Set the underline to be proportional to the font size, with a minimum. The default is too thin.
-    'underline-offset-2',
-    'links-accent:underline-offset-4',
-
-    'links-default:decoration-primary/6',
-    'links-default:text-primary-subtle',
-    'links-default:hover:text-primary-strong',
-    'links-default:contrast-more:text-primary',
-    'links-default:contrast-more:hover:text-primary-strong',
-
-    'links-accent:decoration-primary-subtle',
-    'links-accent:hover:decoration-[3px]',
-    'links-accent:hover:[text-decoration-skip-ink:none]',
-
-    'transition-all',
-    'duration-100',
-];
+import type { DesignTokenName } from './StyleProvider';
 
 /**
  * Styled version of Link component.
  */
 export function StyledLink(props: Omit<LinkProps, 'style'> & { className?: ClassValue }) {
-    const { className, ...rest } = props;
+    const { classNames, ...rest } = props;
+
+    const classNamesToForward: DesignTokenName[] = [...(classNames || []), 'LinkStyles'];
 
     return (
-        <Link {...rest} className={tcls(linkStyles, className)}>
+        <Link {...rest} classNames={classNamesToForward}>
             {props.children}
         </Link>
     );
diff --git a/packages/gitbook/src/components/primitives/styles.ts b/packages/gitbook/src/components/primitives/styles.ts
new file mode 100644
index 0000000000..9d465aac4a
--- /dev/null
+++ b/packages/gitbook/src/components/primitives/styles.ts
@@ -0,0 +1,74 @@
+import type { ClassValue } from '@/lib/tailwind';
+
+export const ButtonStyles = [
+    'button',
+    'inline-flex',
+    'items-center',
+    'gap-2',
+    'rounded-md',
+    'straight-corners:rounded-none',
+    'circular-corners:rounded-full',
+    // 'place-self-start',
+
+    'ring-1',
+    'ring-tint',
+    'hover:ring-tint-hover',
+
+    'shadow-sm',
+    'shadow-tint',
+    'dark:shadow-tint-1',
+    'hover:shadow-md',
+    'active:shadow-none',
+    'depth-flat:shadow-none',
+
+    'contrast-more:ring-tint-12',
+    'contrast-more:hover:ring-2',
+    'contrast-more:hover:ring-tint-12',
+
+    'hover:scale-104',
+    'depth-flat:hover:scale-100',
+    'active:scale-100',
+    'transition-all',
+
+    'grow-0',
+    'shrink-0',
+    'truncate',
+] as ClassValue[];
+
+export const CardStyles = [
+    'group',
+    'flex',
+    'flex-row',
+    'justify-between',
+    'items-center',
+    'gap-4',
+    'ring-1',
+    'ring-tint-subtle',
+    'rounded',
+    'straight-corners:rounded-none',
+    'circular-corners:rounded-2xl',
+    'px-5',
+    'py-3',
+    'transition-shadow',
+    'hover:ring-primary-hover',
+] as ClassValue[];
+
+export const LinkStyles = [
+    'underline',
+    'decoration-[max(0.07em,1px)]', // Set the underline to be proportional to the font size, with a minimum. The default is too thin.
+    'underline-offset-2',
+    'links-accent:underline-offset-4',
+
+    'links-default:decoration-primary/6',
+    'links-default:text-primary-subtle',
+    'links-default:hover:text-primary-strong',
+    'links-default:contrast-more:text-primary',
+    'links-default:contrast-more:hover:text-primary-strong',
+
+    'links-accent:decoration-primary-subtle',
+    'links-accent:hover:decoration-[3px]',
+    'links-accent:hover:[text-decoration-skip-ink:none]',
+
+    'transition-all',
+    'duration-100',
+] as ClassValue[];
diff --git a/packages/gitbook/src/fonts/custom.test.ts b/packages/gitbook/src/fonts/custom.test.ts
index ce2616187e..b430b76241 100644
--- a/packages/gitbook/src/fonts/custom.test.ts
+++ b/packages/gitbook/src/fonts/custom.test.ts
@@ -28,6 +28,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     multiWeight: {
@@ -81,6 +84,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     multiSource: {
@@ -99,6 +105,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     missingFormat: {
@@ -117,6 +126,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     empty: {
@@ -124,6 +136,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
         custom: true,
         fontFamily: 'Empty Font',
         fontFaces: [],
+        permissions: {
+            edit: false,
+        },
     },
 
     specialChars: {
@@ -136,6 +151,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 sources: [{ url: 'https://example.com/fonts/special.woff2', format: 'woff2' }],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     complex: {
@@ -158,6 +176,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 
     variousURLs: {
@@ -174,6 +195,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
                 ],
             },
         ],
+        permissions: {
+            edit: false,
+        },
     },
 };
 
diff --git a/packages/gitbook/src/fonts/custom.ts b/packages/gitbook/src/fonts/custom.ts
index cb31f771ed..84abdf8b27 100644
--- a/packages/gitbook/src/fonts/custom.ts
+++ b/packages/gitbook/src/fonts/custom.ts
@@ -1,9 +1,9 @@
-import type { CustomizationFontDefinition } from '@gitbook/api';
+import type { CustomizationFontDefinitionInput } from '@gitbook/api';
 
 /**
  * Define the custom font faces and set the --font-custom to the custom font name
  */
-export function generateFontFacesCSS(customFont: CustomizationFontDefinition): string {
+export function generateFontFacesCSS(customFont: CustomizationFontDefinitionInput): string {
     const { fontFaces } = customFont;
 
     // Generate font face declarations for all weights
@@ -45,7 +45,7 @@ export function generateFontFacesCSS(customFont: CustomizationFontDefinition): s
 /**
  * Get a list of font sources to preload (only 400 and 700 weights)
  */
-export function getFontSourcesToPreload(customFont: CustomizationFontDefinition) {
+export function getFontSourcesToPreload(customFont: CustomizationFontDefinitionInput) {
     return customFont.fontFaces.filter(
         (face): face is typeof face & { weight: 400 | 700 } =>
             face.weight === 400 || face.weight === 700
diff --git a/packages/gitbook/src/intl/translations/de.ts b/packages/gitbook/src/intl/translations/de.ts
index b42da6c55f..9ed91a152c 100644
--- a/packages/gitbook/src/intl/translations/de.ts
+++ b/packages/gitbook/src/intl/translations/de.ts
@@ -40,7 +40,7 @@ export const de = {
     cookies_prompt_privacy: 'Datenschutzrichtlinie',
     cookies_accept: 'Akzeptieren',
     cookies_reject: 'Ablehnen',
-    cookies_close: 'Schließen',
+    close: 'Schließen',
     edit_on_git: 'Bearbeiten auf ${1}',
     notfound_title: 'Seite nicht gefunden',
     notfound: 'Die gesuchte Seite existiert nicht.',
diff --git a/packages/gitbook/src/intl/translations/en.ts b/packages/gitbook/src/intl/translations/en.ts
index df9cc50993..6fa837ebb8 100644
--- a/packages/gitbook/src/intl/translations/en.ts
+++ b/packages/gitbook/src/intl/translations/en.ts
@@ -40,7 +40,7 @@ export const en = {
     cookies_prompt_privacy: 'privacy policy',
     cookies_accept: 'Accept',
     cookies_reject: 'Reject',
-    cookies_close: 'Close',
+    close: 'Close',
     edit_on_git: 'Edit on ${1}',
     notfound_title: 'Page not found',
     notfound: "The page you are looking for doesn't exist.",
diff --git a/packages/gitbook/src/intl/translations/es.ts b/packages/gitbook/src/intl/translations/es.ts
index 2d0d465652..d2ca1bb9d5 100644
--- a/packages/gitbook/src/intl/translations/es.ts
+++ b/packages/gitbook/src/intl/translations/es.ts
@@ -42,7 +42,7 @@ export const es: TranslationLanguage = {
     cookies_prompt_privacy: 'política de privacidad',
     cookies_accept: 'Aceptar',
     cookies_reject: 'Rechazar',
-    cookies_close: 'Cerrar',
+    close: 'Cerrar',
     edit_on_git: 'Editar en ${1}',
     notfound_title: 'Página no encontrada',
     notfound: 'La página que buscas no existe.',
diff --git a/packages/gitbook/src/intl/translations/fr.ts b/packages/gitbook/src/intl/translations/fr.ts
index 71d340332b..922f7554d0 100644
--- a/packages/gitbook/src/intl/translations/fr.ts
+++ b/packages/gitbook/src/intl/translations/fr.ts
@@ -42,7 +42,7 @@ export const fr: TranslationLanguage = {
     cookies_prompt_privacy: 'politique de confidentialité',
     cookies_accept: 'Accepter',
     cookies_reject: 'Rejeter',
-    cookies_close: 'Fermer',
+    close: 'Fermer',
     edit_on_git: 'Modifier sur ${1}',
     notfound_title: 'Page non trouvée',
     notfound: "La page que vous cherchez n'existe pas.",
diff --git a/packages/gitbook/src/intl/translations/ja.ts b/packages/gitbook/src/intl/translations/ja.ts
index f3f5480689..6eaa789ed8 100644
--- a/packages/gitbook/src/intl/translations/ja.ts
+++ b/packages/gitbook/src/intl/translations/ja.ts
@@ -42,7 +42,7 @@ export const ja: TranslationLanguage = {
     cookies_prompt_privacy: 'プライバシーポリシー',
     cookies_accept: '同意する',
     cookies_reject: '拒否する',
-    cookies_close: '閉じる',
+    close: '閉じる',
     edit_on_git: '${1}で編集',
     notfound_title: 'ページが見つかりません',
     notfound: 'お探しのページは存在しません。',
diff --git a/packages/gitbook/src/intl/translations/nl.ts b/packages/gitbook/src/intl/translations/nl.ts
index 6bfb45e9a6..d61bfb71d9 100644
--- a/packages/gitbook/src/intl/translations/nl.ts
+++ b/packages/gitbook/src/intl/translations/nl.ts
@@ -42,7 +42,7 @@ export const nl: TranslationLanguage = {
     cookies_prompt_privacy: 'privacyverklaring',
     cookies_accept: 'Accepteren',
     cookies_reject: 'Weigeren',
-    cookies_close: 'Sluiten',
+    close: 'Sluiten',
     edit_on_git: 'Bewerk op ${1}',
     notfound_title: 'Pagina niet gevonden',
     notfound: 'De pagina die je zoekt, bestaat niet.',
diff --git a/packages/gitbook/src/intl/translations/no.ts b/packages/gitbook/src/intl/translations/no.ts
index 6be6b413e3..ddd94e5466 100644
--- a/packages/gitbook/src/intl/translations/no.ts
+++ b/packages/gitbook/src/intl/translations/no.ts
@@ -42,7 +42,7 @@ export const no: TranslationLanguage = {
     cookies_prompt_privacy: 'personvernerklæringen',
     cookies_accept: 'Godta',
     cookies_reject: 'Avslå',
-    cookies_close: 'Lukk',
+    close: 'Lukk',
     edit_on_git: 'Rediger på ${1}',
     notfound_title: 'Siden ble ikke funnet',
     notfound: 'Siden du leter etter eksisterer ikke.',
diff --git a/packages/gitbook/src/intl/translations/pt-br.ts b/packages/gitbook/src/intl/translations/pt-br.ts
index 35a40cd45f..c3e2244785 100644
--- a/packages/gitbook/src/intl/translations/pt-br.ts
+++ b/packages/gitbook/src/intl/translations/pt-br.ts
@@ -40,7 +40,7 @@ export const pt_br = {
     cookies_prompt_privacy: 'política de privacidade',
     cookies_accept: 'Aceitar',
     cookies_reject: 'Rejeitar',
-    cookies_close: 'Fechar',
+    close: 'Fechar',
     edit_on_git: 'Editar no ${1}',
     notfound_title: 'Página não encontrada',
     notfound: 'A página que você está procurando não existe.',
diff --git a/packages/gitbook/src/intl/translations/zh.ts b/packages/gitbook/src/intl/translations/zh.ts
index 08efdddde6..096b0d8cb3 100644
--- a/packages/gitbook/src/intl/translations/zh.ts
+++ b/packages/gitbook/src/intl/translations/zh.ts
@@ -40,7 +40,7 @@ export const zh: TranslationLanguage = {
     cookies_prompt_privacy: '隐私政策',
     cookies_accept: '接受',
     cookies_reject: '拒绝',
-    cookies_close: '关闭',
+    close: '关闭',
     edit_on_git: '在${1}上编辑',
     notfound_title: '页面未找到',
     notfound: '您要找的页面不存在。',
diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts
index 2bb2039bc3..cb612664b0 100644
--- a/packages/gitbook/src/lib/api.ts
+++ b/packages/gitbook/src/lib/api.ts
@@ -267,6 +267,7 @@ export const getPublishedContentByUrl = cache({
 
             const parsed = parseCacheResponse(response);
 
+            // biome-ignore lint/suspicious/noConsole: log the ttl of the token
             console.log(
                 `Parsed ttl: ${parsed.ttl} at ${Date.now()}, for ${'apiToken' in response.data ? response.data.apiToken : '<no-token>'}`
             );
@@ -492,6 +493,39 @@ export const getRevisionPageByPath = cache({
     },
 });
 
+/**
+ * Get a document from a page by its ID
+ */
+export const getRevisionPageDocument = cache({
+    name: 'api.getRevisionPageDocument.v1',
+    tag: (spaceId, revisionId) =>
+        getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }),
+    tagImmutable: true,
+    getKeySuffix: getAPIContextId,
+    get: async (
+        spaceId: string,
+        revisionId: string,
+        pageId: string,
+        options: CacheFunctionOptions
+    ) => {
+        const apiCtx = await api();
+        const response = await apiCtx.client.spaces.getPageDocumentInRevisionById(
+            spaceId,
+            revisionId,
+            pageId,
+            {
+                evaluated: true,
+            },
+            {
+                ...noCacheFetchOptions,
+                signal: options.signal,
+            }
+        );
+
+        return cacheResponse(response, cacheTtl_7days);
+    },
+});
+
 /**
  * Resolve a file by its ID.
  * It should not be used directly, use `getRevisionFile` instead.
@@ -764,7 +798,12 @@ export const getComputedDocument = cache({
  * Mimic the validation done on source server-side to reduce API usage.
  */
 function validateSiteRedirectSource(source: string) {
-    return source.length <= 512 && /^\/[a-zA-Z0-9-_.\\/]+[a-zA-Z0-9-_.]$/.test(source);
+    return (
+        source.length <= 512 &&
+        /^\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+)*$/.test(
+            source
+        )
+    );
 }
 
 /**
diff --git a/packages/gitbook/src/lib/customization.ts b/packages/gitbook/src/lib/customization.ts
index 38440234b1..d23a0a88cb 100644
--- a/packages/gitbook/src/lib/customization.ts
+++ b/packages/gitbook/src/lib/customization.ts
@@ -12,9 +12,12 @@ export async function getDynamicCustomizationSettings(
 ): Promise<SiteCustomizationSettings> {
     const headersList = await headers();
     const extend = headersList.get(MiddlewareHeaders.Customization);
+
     if (extend) {
         try {
-            const parsedSettings = rison.decode_object<SiteCustomizationSettings>(extend);
+            // We need to decode it first as it is URL encoded, then decode the Rison object
+            const unencoded = decodeURIComponent(extend);
+            const parsedSettings = rison.decode_object<SiteCustomizationSettings>(unencoded);
 
             return parsedSettings;
         } catch (_error) {}
diff --git a/packages/gitbook/src/lib/document-sections.ts b/packages/gitbook/src/lib/document-sections.ts
index 38619a5881..9e59122365 100644
--- a/packages/gitbook/src/lib/document-sections.ts
+++ b/packages/gitbook/src/lib/document-sections.ts
@@ -1,4 +1,4 @@
-import type { JSONDocument } from '@gitbook/api';
+import type { DocumentBlock, JSONDocument } from '@gitbook/api';
 import type { GitBookAnyContext } from '@v2/lib/context';
 
 import { getNodeText } from './document';
@@ -19,58 +19,102 @@ export interface DocumentSection {
 export async function getDocumentSections(
     context: GitBookAnyContext,
     document: JSONDocument
+): Promise<DocumentSection[]> {
+    return getSectionsFromNodes(document.nodes, context);
+}
+
+/**
+ * Extract a list of sections from a list of nodes.
+ */
+async function getSectionsFromNodes(
+    nodes: DocumentBlock[],
+    context: GitBookAnyContext
 ): Promise<DocumentSection[]> {
     const sections: DocumentSection[] = [];
     let depth = 0;
 
-    for (const block of document.nodes) {
-        if ((block.type === 'heading-1' || block.type === 'heading-2') && block.meta?.id) {
-            if (block.type === 'heading-1') {
+    for (const block of nodes) {
+        switch (block.type) {
+            case 'heading-1': {
+                const id = block.meta?.id;
+                if (!id) {
+                    continue;
+                }
                 depth = 1;
-            }
-            const title = getNodeText(block);
-            const id = block.meta.id;
-
-            sections.push({
-                id,
-                title,
-                depth: block.type === 'heading-1' ? 1 : depth > 0 ? 2 : 1,
-            });
-        }
-
-        if ((block.type === 'swagger' || block.type === 'openapi-operation') && block.meta?.id) {
-            const { data: operation } = await resolveOpenAPIOperationBlock({
-                block,
-                context,
-            });
-            if (operation) {
+                const title = getNodeText(block);
                 sections.push({
-                    id: block.meta.id,
-                    tag: operation.method.toUpperCase(),
-                    title: operation.operation.summary || operation.path,
+                    id,
+                    title,
                     depth: 1,
-                    deprecated: operation.operation.deprecated,
                 });
+                continue;
             }
-        }
-
-        if (
-            block.type === 'openapi-schemas' &&
-            !block.data.grouped &&
-            block.meta?.id &&
-            block.data.schemas.length === 1
-        ) {
-            const { data } = await resolveOpenAPISchemasBlock({
-                block,
-                context,
-            });
-            const schema = data?.schemas[0];
-            if (schema) {
+            case 'heading-2': {
+                const id = block.meta?.id;
+                if (!id) {
+                    continue;
+                }
+                const title = getNodeText(block);
                 sections.push({
-                    id: block.meta.id,
-                    title: `The ${schema.name} object`,
-                    depth: 1,
+                    id,
+                    title,
+                    depth: depth > 0 ? 2 : 1,
+                });
+                continue;
+            }
+            case 'stepper': {
+                const stepNodes = await Promise.all(
+                    block.nodes.map(async (step) => getSectionsFromNodes(step.nodes, context))
+                );
+                for (const stepSections of stepNodes) {
+                    sections.push(...stepSections);
+                }
+                continue;
+            }
+            case 'swagger':
+            case 'openapi-operation': {
+                const id = block.meta?.id;
+                if (!id) {
+                    continue;
+                }
+                const { data: operation } = await resolveOpenAPIOperationBlock({
+                    block,
+                    context,
+                });
+                if (operation) {
+                    sections.push({
+                        id,
+                        tag: operation.method.toUpperCase(),
+                        title: operation.operation.summary || operation.path,
+                        depth: 1,
+                        deprecated: operation.operation.deprecated,
+                    });
+                }
+                continue;
+            }
+            case 'openapi-schemas': {
+                const id = block.meta?.id;
+                if (!id) {
+                    continue;
+                }
+                if (block.data.grouped || block.data.schemas.length !== 1) {
+                    // Skip grouped schemas, they are not sections
+                    continue;
+                }
+
+                const { data } = await resolveOpenAPISchemasBlock({
+                    block,
+                    context,
                 });
+                const schema = data?.schemas[0];
+                if (schema) {
+                    sections.push({
+                        id,
+                        title: `The ${schema.name} object`,
+                        depth: 1,
+                    });
+                }
+                continue;
             }
         }
     }
diff --git a/packages/gitbook/src/lib/document.ts b/packages/gitbook/src/lib/document.ts
index 33618f76c1..8d9fabc1e5 100644
--- a/packages/gitbook/src/lib/document.ts
+++ b/packages/gitbook/src/lib/document.ts
@@ -30,6 +30,41 @@ export function hasFullWidthBlock(document: JSONDocument): boolean {
     return false;
 }
 
+/**
+ * Returns true if the document has more than `limit` blocks and/or inlines that match the `check` predicate.
+ */
+export function hasMoreThan(
+    document: JSONDocument | DocumentBlock,
+    check: (block: DocumentBlock | DocumentInline) => boolean,
+    limit = 1
+): boolean {
+    let count = 0;
+
+    function traverse(node: JSONDocument | DocumentBlock | DocumentFragment): boolean {
+        for (const child of 'nodes' in node ? node.nodes : []) {
+            if (child.object === 'text') continue;
+
+            if (check(child)) {
+                count++;
+                if (count > limit) return true;
+            }
+
+            if (child.object === 'block' && 'nodes' in child) {
+                if (traverse(child)) return true;
+            }
+
+            if (child.object === 'block' && 'fragments' in child) {
+                for (const fragment of child.fragments) {
+                    if (traverse(fragment)) return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    return traverse(document);
+}
+
 /**
  * Get the text of a block/inline.
  */
diff --git a/packages/gitbook/src/lib/openapi/fetch.ts b/packages/gitbook/src/lib/openapi/fetch.ts
index d1f8c03cef..889ea742ae 100644
--- a/packages/gitbook/src/lib/openapi/fetch.ts
+++ b/packages/gitbook/src/lib/openapi/fetch.ts
@@ -1,5 +1,4 @@
 import { parseOpenAPI } from '@gitbook/openapi-parser';
-import { unstable_cache } from 'next/cache';
 
 import { type CacheFunctionOptions, cache, noCacheFetchOptions } from '@/lib/cache';
 import type {
@@ -8,9 +7,6 @@ import type {
     OpenAPIWebhookBlock,
     ResolveOpenAPIBlockArgs,
 } from '@/lib/openapi/types';
-import { getCloudflareRequestGlobal } from '@v2/lib/data/cloudflare';
-import { withCacheKey, withoutConcurrentExecution } from '@v2/lib/data/memoize';
-import { GITBOOK_RUNTIME } from '@v2/lib/env';
 import { assert } from 'ts-essentials';
 import { resolveContentRef } from '../references';
 import { isV2 } from '../v2';
@@ -50,7 +46,7 @@ export async function fetchOpenAPIFilesystem(
 
 function fetchFilesystem(url: string) {
     if (isV2()) {
-        return fetchFilesystemV2(url);
+        return fetchFilesystemUseCache(url);
     }
 
     return fetchFilesystemV1(url);
@@ -70,22 +66,6 @@ const fetchFilesystemV1 = cache({
     },
 });
 
-const fetchFilesystemV2 = withCacheKey(
-    withoutConcurrentExecution(getCloudflareRequestGlobal, async (cacheKey, url: string) => {
-        if (GITBOOK_RUNTIME !== 'cloudflare') {
-            return fetchFilesystemUseCache(url);
-        }
-
-        // FIXME: OpenNext doesn't support 'use cache' yet
-        const uncached = unstable_cache(async () => fetchFilesystemUncached(url), [cacheKey], {
-            revalidate: 60 * 60 * 24,
-        });
-
-        const response = await uncached();
-        return response;
-    })
-);
-
 const fetchFilesystemUseCache = async (url: string) => {
     'use cache';
     return fetchFilesystemUncached(url);
diff --git a/packages/gitbook/src/lib/paths.test.ts b/packages/gitbook/src/lib/paths.test.ts
new file mode 100644
index 0000000000..1d418f4988
--- /dev/null
+++ b/packages/gitbook/src/lib/paths.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from 'bun:test';
+import { getExtension } from './paths';
+
+describe('getExtension', () => {
+    it('should return the extension of a path', () => {
+        expect(getExtension('test.txt')).toBe('.txt');
+    });
+
+    it('should return an empty string if there is no extension', () => {
+        expect(getExtension('test/path/to/file')).toBe('');
+    });
+
+    it('should return the extension of a path with multiple dots', () => {
+        expect(getExtension('test.with.multiple.dots.txt')).toBe('.txt');
+    });
+});
diff --git a/packages/gitbook/src/lib/paths.ts b/packages/gitbook/src/lib/paths.ts
index eff69a569b..ebdf35cd3f 100644
--- a/packages/gitbook/src/lib/paths.ts
+++ b/packages/gitbook/src/lib/paths.ts
@@ -57,3 +57,15 @@ export function withTrailingSlash(pathname: string): string {
 
     return pathname;
 }
+
+/**
+ * Get the extension of a path.
+ */
+export function getExtension(path: string): string {
+    const re = /\.[0-9a-z]+$/i;
+    const match = path.match(re);
+    if (match) {
+        return match[0];
+    }
+    return '';
+}
diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx
index ab941e43c5..7362decbe6 100644
--- a/packages/gitbook/src/lib/references.tsx
+++ b/packages/gitbook/src/lib/references.tsx
@@ -41,8 +41,12 @@ export interface ResolvedContentRef {
     file?: RevisionFile;
     /** Page document resolved from the content ref */
     page?: RevisionPageDocument;
-    /** Resolved reusable content, if the ref points to reusable content on a revision. */
-    reusableContent?: RevisionReusableContent;
+    /** Resolved reusable content, if the ref points to reusable content on a revision. Also contains the space and revision used for resolution. */
+    reusableContent?: {
+        revisionReusableContent: RevisionReusableContent;
+        space: Space;
+        revision: string;
+    };
     /** Resolve OpenAPI spec filesystem. */
     openAPIFilesystem?: Filesystem;
 }
@@ -143,7 +147,7 @@ export async function resolveContentRef(
 
             // Compute the text to display for the link
             if (anchor) {
-                text = `#${anchor}`;
+                text = page.title;
                 ancestors.push({
                     label: page.title,
                     icon: <PageIcon page={page} style={iconStyle} />,
@@ -151,7 +155,7 @@ export async function resolveContentRef(
                 });
 
                 if (resolveAnchorText) {
-                    const document = await getPageDocument(dataFetcher, space, page);
+                    const document = await getPageDocument(context, page);
                     if (document) {
                         const block = getBlockById(document, anchor);
                         if (block) {
@@ -231,21 +235,52 @@ export async function resolveContentRef(
         }
 
         case 'reusable-content': {
+            // Figure out which space and revision the reusable content is in.
+            const container: { space: Space; revision: string } | null = await (async () => {
+                // without a space on the content ref, or if the space is the same as the current one, we can use the current revision.
+                if (!contentRef.space || contentRef.space === context.space.id) {
+                    return { space: context.space, revision: revisionId };
+                }
+
+                const space = await getDataOrNull(
+                    dataFetcher.getSpace({
+                        spaceId: contentRef.space,
+                        shareKey: undefined,
+                    })
+                );
+
+                if (!space) {
+                    return null;
+                }
+
+                return { space, revision: space.revision };
+            })();
+
+            if (!container) {
+                return null;
+            }
+
             const reusableContent = await getDataOrNull(
                 dataFetcher.getReusableContent({
-                    spaceId: space.id,
-                    revisionId,
+                    spaceId: container.space.id,
+                    revisionId: container.revision,
                     reusableContentId: contentRef.reusableContent,
                 })
             );
+
             if (!reusableContent) {
                 return null;
             }
+
             return {
-                href: getGitBookAppHref(`/s/${space.id}`),
+                href: getGitBookAppHref(`/s/${container.space}/~/reusable/${reusableContent.id}`),
                 text: reusableContent.title,
                 active: false,
-                reusableContent,
+                reusableContent: {
+                    revisionReusableContent: reusableContent,
+                    space: container.space,
+                    revision: container.revision,
+                },
             };
         }
 
diff --git a/packages/gitbook/src/lib/sites.ts b/packages/gitbook/src/lib/sites.ts
index 88d63e75d3..1827683e48 100644
--- a/packages/gitbook/src/lib/sites.ts
+++ b/packages/gitbook/src/lib/sites.ts
@@ -1,4 +1,6 @@
 import type { SiteSection, SiteSectionGroup, SiteSpace, SiteStructure } from '@gitbook/api';
+import type { GitBookSiteContext } from '@v2/lib/context';
+import { joinPath } from './paths';
 
 /**
  * Get all sections from a site structure.
@@ -64,6 +66,34 @@ export function findSiteSpaceById(siteStructure: SiteStructure, spaceId: string)
     return null;
 }
 
+/**
+ * Get the URL to navigate to for a section.
+ * When the site is not published yet, `urls.published` is not available.
+ * To ensure navigation works in preview, we compute a relative URL from the siteSection path.
+ */
+export function getSectionURL(context: GitBookSiteContext, section: SiteSection) {
+    const { linker } = context;
+    return section.urls.published
+        ? linker.toLinkForContent(section.urls.published)
+        : linker.toPathInSite(section.path);
+}
+
+/**
+ * Get the URL to navigate to for a site space.
+ * When the site is not published yet, `urls.published` is not available.
+ * To ensure navigation works in preview, we compute a relative URL from the siteSpace path.
+ */
+export function getSiteSpaceURL(context: GitBookSiteContext, siteSpace: SiteSpace) {
+    const { linker, sections } = context;
+    if (siteSpace.urls.published) {
+        return linker.toLinkForContent(siteSpace.urls.published);
+    }
+
+    return linker.toPathInSite(
+        sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path
+    );
+}
+
 function findSiteSpaceByIdInSections(sections: SiteSection[], spaceId: string): SiteSpace | null {
     for (const section of sections) {
         const siteSpace =
diff --git a/packages/gitbook/src/lib/tracing.ts b/packages/gitbook/src/lib/tracing.ts
index 38db339240..71b5c072e8 100644
--- a/packages/gitbook/src/lib/tracing.ts
+++ b/packages/gitbook/src/lib/tracing.ts
@@ -28,19 +28,19 @@ export async function trace<T>(
     };
 
     const start = now();
-    let failed = false;
+    let traceError: null | Error = null;
     try {
         return await fn(span);
     } catch (error) {
         span.setAttribute('error', true);
-        failed = true;
+        traceError = error as Error;
         throw error;
     } finally {
         if (process.env.SILENT !== 'true' && process.env.NODE_ENV !== 'development') {
             const end = now();
             // biome-ignore lint/suspicious/noConsole: we want to log performance data
             console.log(
-                `trace ${completeName} ${failed ? 'failed' : 'succeeded'} in ${end - start}ms`,
+                `trace ${completeName} ${traceError ? `failed with ${traceError.message}` : 'succeeded'} in ${end - start}ms`,
                 attributes
             );
         }
diff --git a/packages/gitbook/src/lib/typescript.ts b/packages/gitbook/src/lib/typescript.ts
index 43940d4a2a..f13574a5a4 100644
--- a/packages/gitbook/src/lib/typescript.ts
+++ b/packages/gitbook/src/lib/typescript.ts
@@ -4,3 +4,10 @@
 export function filterOutNullable<T>(value: T): value is NonNullable<T> {
     return !!value;
 }
+
+/**
+ * Alternative to `assertNever` that returns `null` instead of throwing an error.
+ */
+export function nullIfNever(_value: never): null {
+    return null;
+}
diff --git a/packages/gitbook/src/lib/urls.ts b/packages/gitbook/src/lib/urls.ts
index 0bfa89e769..05dba4f3d0 100644
--- a/packages/gitbook/src/lib/urls.ts
+++ b/packages/gitbook/src/lib/urls.ts
@@ -8,3 +8,17 @@ export function checkIsHttpURL(input: string | URL): boolean {
     const parsed = new URL(input);
     return parsed.protocol === 'http:' || parsed.protocol === 'https:';
 }
+
+/**
+ * True for absolute URLs (`scheme:*`) or hash-only anchors.
+ */
+export function checkIsExternalURL(input: string): boolean {
+    return URL.canParse(input);
+}
+
+/**
+ * True for a hash-only anchor.
+ */
+export function checkIsAnchor(input: string): boolean {
+    return input.startsWith('#');
+}
diff --git a/packages/gitbook/src/lib/utils.ts b/packages/gitbook/src/lib/utils.ts
index 0db784cf2d..d7a98610d0 100644
--- a/packages/gitbook/src/lib/utils.ts
+++ b/packages/gitbook/src/lib/utils.ts
@@ -17,6 +17,7 @@ export function defaultCustomization(): api.SiteCustomizationSettings {
             background: api.CustomizationBackground.Plain,
             icons: api.CustomizationIconsStyle.Regular,
             links: api.CustomizationLinksStyle.Default,
+            depth: api.CustomizationDepth.Subtle,
             sidebar: {
                 background: api.CustomizationSidebarBackgroundStyle.Default,
                 list: api.CustomizationSidebarListStyle.Default,
diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts
index ea5219816b..db6118a84c 100644
--- a/packages/gitbook/src/lib/v1.ts
+++ b/packages/gitbook/src/lib/v1.ts
@@ -8,6 +8,7 @@ import type { GitBookDataFetcher } from '@v2/lib/data/types';
 import { createImageResizer } from '@v2/lib/images';
 import { createLinker } from '@v2/lib/links';
 
+import { GitBookAPI } from '@gitbook/api';
 import { DataFetcherError, wrapDataFetcherError } from '@v2/lib/data';
 import { headers } from 'next/headers';
 import {
@@ -24,12 +25,14 @@ import {
     getRevision,
     getRevisionFile,
     getRevisionPageByPath,
+    getRevisionPageDocument,
     getRevisionPages,
     getSiteRedirectBySource,
     getSpace,
     getUserById,
     renderIntegrationUi,
     searchSiteContent,
+    withAPI as withAPIV1,
 } from './api';
 import { getDynamicCustomizationSettings } from './customization';
 import { withLeadingSlash, withTrailingSlash } from './paths';
@@ -58,7 +61,7 @@ export async function getV1BaseContext(): Promise<GitBookBaseContext> {
         return url;
     };
 
-    const dataFetcher = await getDataFetcherV1();
+    const dataFetcher = getDataFetcherV1();
 
     const imageResizer = createImageResizer({
         imagesContextId: host,
@@ -82,77 +85,121 @@ export async function getV1BaseContext(): Promise<GitBookBaseContext> {
  * Try not to use this as much as possible, and instead take the data fetcher from the props.
  * This data fetcher should only be used at the top of the tree.
  */
-async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
+function getDataFetcherV1(apiTokenOverride?: string): GitBookDataFetcher {
+    let apiClient: GitBookAPI | undefined;
+
+    /**
+     * Run a function with the correct API client. If an API token is provided, we
+     * create a new API client with the token. Otherwise, we use the default API client.
+     */
+    async function withAPI<T>(fn: () => Promise<T>): Promise<T> {
+        // No token override - we can use the default API client.
+        if (!apiTokenOverride) {
+            return fn();
+        }
+
+        const client = await api();
+
+        if (!apiClient) {
+            // New client uses same endpoint and user agent as the default client.
+            apiClient = new GitBookAPI({
+                endpoint: client.client.endpoint,
+                authToken: apiTokenOverride,
+                userAgent: client.client.userAgent,
+            });
+        }
+
+        return withAPIV1(
+            {
+                client: apiClient,
+                contextId: client.contextId,
+            },
+            fn
+        );
+    }
+
     const dataFetcher: GitBookDataFetcher = {
         async api() {
-            const result = await api();
-            return result.client;
+            return withAPI(async () => {
+                const result = await api();
+                return result.client;
+            });
         },
 
-        withToken() {
-            // In v1, the token is global and controlled by the middleware.
-            // We don't need to do anything special here.
-            return dataFetcher;
+        withToken({ apiToken }) {
+            return getDataFetcherV1(apiToken);
         },
 
         getUserById(userId) {
-            return wrapDataFetcherError(async () => {
-                const user = await getUserById(userId);
-                if (!user) {
-                    throw new DataFetcherError('User not found', 404);
-                }
-
-                return user;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const user = await getUserById(userId);
+                    if (!user) {
+                        throw new DataFetcherError('User not found', 404);
+                    }
+
+                    return user;
+                })
+            );
         },
 
         getPublishedContentSite(params) {
-            return wrapDataFetcherError(async () => {
-                return getPublishedContentSite(params);
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    return getPublishedContentSite(params);
+                })
+            );
         },
 
         getSpace(params) {
-            return wrapDataFetcherError(async () => {
-                return getSpace(params.spaceId, params.shareKey);
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    return getSpace(params.spaceId, params.shareKey);
+                })
+            );
         },
 
         getChangeRequest(params) {
-            return wrapDataFetcherError(async () => {
-                const changeRequest = await getChangeRequest(
-                    params.spaceId,
-                    params.changeRequestId
-                );
-                if (!changeRequest) {
-                    throw new DataFetcherError('Change request not found', 404);
-                }
-
-                return changeRequest;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const changeRequest = await getChangeRequest(
+                        params.spaceId,
+                        params.changeRequestId
+                    );
+                    if (!changeRequest) {
+                        throw new DataFetcherError('Change request not found', 404);
+                    }
+
+                    return changeRequest;
+                })
+            );
         },
 
         getRevision(params) {
-            return wrapDataFetcherError(async () => {
-                return getRevision(params.spaceId, params.revisionId, {
-                    metadata: params.metadata,
-                });
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    return getRevision(params.spaceId, params.revisionId, {
+                        metadata: params.metadata,
+                    });
+                })
+            );
         },
 
         getRevisionFile(params) {
-            return wrapDataFetcherError(async () => {
-                const revisionFile = await getRevisionFile(
-                    params.spaceId,
-                    params.revisionId,
-                    params.fileId
-                );
-                if (!revisionFile) {
-                    throw new DataFetcherError('Revision file not found', 404);
-                }
-
-                return revisionFile;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const revisionFile = await getRevisionFile(
+                        params.spaceId,
+                        params.revisionId,
+                        params.fileId
+                    );
+                    if (!revisionFile) {
+                        throw new DataFetcherError('Revision file not found', 404);
+                    }
+
+                    return revisionFile;
+                })
+            );
         },
 
         getRevisionPageMarkdown() {
@@ -160,117 +207,152 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
         },
 
         getDocument(params) {
-            return wrapDataFetcherError(async () => {
-                const document = await getDocument(params.spaceId, params.documentId);
-                if (!document) {
-                    throw new DataFetcherError('Document not found', 404);
-                }
-
-                return document;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const document = await getDocument(params.spaceId, params.documentId);
+                    if (!document) {
+                        throw new DataFetcherError('Document not found', 404);
+                    }
+
+                    return document;
+                })
+            );
         },
 
         getComputedDocument(params) {
-            return wrapDataFetcherError(() => {
-                return getComputedDocument(
-                    params.organizationId,
-                    params.spaceId,
-                    params.source,
-                    params.seed
-                );
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(() => {
+                    return getComputedDocument(
+                        params.organizationId,
+                        params.spaceId,
+                        params.source,
+                        params.seed
+                    );
+                })
+            );
         },
 
         getRevisionPages(params) {
-            return wrapDataFetcherError(async () => {
-                return getRevisionPages(params.spaceId, params.revisionId, {
-                    metadata: params.metadata,
-                });
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    return getRevisionPages(params.spaceId, params.revisionId, {
+                        metadata: params.metadata,
+                    });
+                })
+            );
+        },
+
+        getRevisionPageDocument(params) {
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    return getRevisionPageDocument(
+                        params.spaceId,
+                        params.revisionId,
+                        params.pageId
+                    );
+                })
+            );
         },
 
         getRevisionPageByPath(params) {
-            return wrapDataFetcherError(async () => {
-                const revisionPage = await getRevisionPageByPath(
-                    params.spaceId,
-                    params.revisionId,
-                    params.path
-                );
-
-                if (!revisionPage) {
-                    throw new DataFetcherError('Revision page not found', 404);
-                }
-
-                return revisionPage;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const revisionPage = await getRevisionPageByPath(
+                        params.spaceId,
+                        params.revisionId,
+                        params.path
+                    );
+
+                    if (!revisionPage) {
+                        throw new DataFetcherError('Revision page not found', 404);
+                    }
+
+                    return revisionPage;
+                })
+            );
         },
 
         getReusableContent(params) {
-            return wrapDataFetcherError(async () => {
-                const reusableContent = await getReusableContent(
-                    params.spaceId,
-                    params.revisionId,
-                    params.reusableContentId
-                );
-
-                if (!reusableContent) {
-                    throw new DataFetcherError('Reusable content not found', 404);
-                }
-
-                return reusableContent;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const reusableContent = await getReusableContent(
+                        params.spaceId,
+                        params.revisionId,
+                        params.reusableContentId
+                    );
+
+                    if (!reusableContent) {
+                        throw new DataFetcherError('Reusable content not found', 404);
+                    }
+
+                    return reusableContent;
+                })
+            );
         },
 
         getLatestOpenAPISpecVersionContent(params) {
-            return wrapDataFetcherError(async () => {
-                const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent(
-                    params.organizationId,
-                    params.slug
-                );
-
-                if (!openAPISpecVersionContent) {
-                    throw new DataFetcherError('OpenAPI spec version content not found', 404);
-                }
-
-                return openAPISpecVersionContent;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent(
+                        params.organizationId,
+                        params.slug
+                    );
+
+                    if (!openAPISpecVersionContent) {
+                        throw new DataFetcherError('OpenAPI spec version content not found', 404);
+                    }
+
+                    return openAPISpecVersionContent;
+                })
+            );
         },
 
         getSiteRedirectBySource(params) {
-            return wrapDataFetcherError(async () => {
-                const siteRedirect = await getSiteRedirectBySource(params);
-                if (!siteRedirect) {
-                    throw new DataFetcherError('Site redirect not found', 404);
-                }
-
-                return siteRedirect;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const siteRedirect = await getSiteRedirectBySource(params);
+                    if (!siteRedirect) {
+                        throw new DataFetcherError('Site redirect not found', 404);
+                    }
+
+                    return siteRedirect;
+                })
+            );
         },
         getEmbedByUrl(params) {
-            return wrapDataFetcherError(() => {
-                return getEmbedByUrlInSpace(params.spaceId, params.url);
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(() => {
+                    return getEmbedByUrlInSpace(params.spaceId, params.url);
+                })
+            );
         },
 
         searchSiteContent(params) {
-            return wrapDataFetcherError(async () => {
-                const { organizationId, siteId, query, cacheBust, scope } = params;
-                const result = await searchSiteContent(
-                    organizationId,
-                    siteId,
-                    query,
-                    scope,
-                    cacheBust
-                );
-                return result.items;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const { organizationId, siteId, query, cacheBust, scope } = params;
+                    const result = await searchSiteContent(
+                        organizationId,
+                        siteId,
+                        query,
+                        scope,
+                        cacheBust
+                    );
+                    return result.items;
+                })
+            );
         },
 
         renderIntegrationUi(params) {
-            return wrapDataFetcherError(async () => {
-                const result = await renderIntegrationUi(params.integrationName, params.request);
-                return result;
-            });
+            return withAPI(() =>
+                wrapDataFetcherError(async () => {
+                    const result = await renderIntegrationUi(
+                        params.integrationName,
+                        params.request
+                    );
+                    return result;
+                })
+            );
         },
 
         streamAIResponse() {
diff --git a/packages/gitbook/src/lib/visitor-token.test.ts b/packages/gitbook/src/lib/visitors.test.ts
similarity index 56%
rename from packages/gitbook/src/lib/visitor-token.test.ts
rename to packages/gitbook/src/lib/visitors.test.ts
index a1f07d2504..4337371fa6 100644
--- a/packages/gitbook/src/lib/visitor-token.test.ts
+++ b/packages/gitbook/src/lib/visitors.test.ts
@@ -6,7 +6,8 @@ import {
     getVisitorAuthCookieName,
     getVisitorAuthCookieValue,
     getVisitorToken,
-} from './visitor-token';
+    getVisitorUnsignedClaims,
+} from './visitors';
 
 describe('getVisitorAuthToken', () => {
     it('should return the token from the query parameters', () => {
@@ -158,3 +159,139 @@ function assertVisitorAuthCookieValue(
 
     throw new Error('Expected a VisitorAuthCookieValue');
 }
+
+describe('getVisitorUnsignedClaims', () => {
+    it('should merge claims from multiple public cookies', () => {
+        const cookies = [
+            {
+                name: 'gitbook-visitor-public-bucket',
+                value: JSON.stringify({ bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } } }),
+            },
+            {
+                name: 'gitbook-visitor-public-launchdarkly',
+                value: JSON.stringify({
+                    launchdarkly: { flags: { ALPHA: true, API: true } },
+                }),
+            },
+        ];
+
+        const url = new URL('https://example.com/');
+        const claims = getVisitorUnsignedClaims({ cookies, url });
+
+        expect(claims.all).toStrictEqual({
+            bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } },
+            launchdarkly: { flags: { ALPHA: true, API: true } },
+        });
+    });
+
+    it('should parse visitor.* query params with simple types', () => {
+        const url = new URL(
+            'https://example.com/?visitor.isEnterprise=true&visitor.language=fr&visitor.country=fr'
+        );
+
+        const claims = getVisitorUnsignedClaims({ cookies: [], url });
+
+        expect(claims.all).toStrictEqual({
+            isEnterprise: true,
+            language: 'fr',
+            country: 'fr',
+        });
+        expect(claims.fromVisitorParams).toStrictEqual({
+            isEnterprise: true,
+            language: 'fr',
+            country: 'fr',
+        });
+    });
+
+    it('should ignore params that do not match visitor.* convention', () => {
+        const url = new URL('https://example.com/?visitor.isEnterprise=true&otherParam=true');
+
+        const claims = getVisitorUnsignedClaims({ cookies: [], url });
+
+        expect(claims.all).toStrictEqual({
+            isEnterprise: true,
+            // otherParam is not present
+        });
+        expect(claims.fromVisitorParams).toStrictEqual({
+            isEnterprise: true,
+        });
+    });
+
+    it('should support nested query param keys via dot notation', () => {
+        const url = new URL(
+            'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false'
+        );
+
+        const claims = getVisitorUnsignedClaims({ cookies: [], url });
+
+        expect(claims.all).toStrictEqual({
+            isEnterprise: true,
+            flags: {
+                ALPHA: true,
+                API: false,
+            },
+        });
+        expect(claims.fromVisitorParams).toStrictEqual({
+            isEnterprise: true,
+            flags: {
+                ALPHA: true,
+                API: false,
+            },
+        });
+    });
+
+    it('should ignore invalid JSON in cookie values', () => {
+        const cookies = [
+            {
+                name: 'gitbook-visitor-public',
+                value: '{not: "json"}',
+            },
+        ];
+        const url = new URL('https://example.com/');
+        const claims = getVisitorUnsignedClaims({ cookies, url });
+
+        expect(claims.all).toStrictEqual({});
+    });
+
+    it('should merge claims from cookies and visitor.* query params', () => {
+        const cookies = [
+            {
+                name: 'gitbook-visitor-public',
+                value: JSON.stringify({ role: 'admin', language: 'fr' }),
+            },
+            {
+                name: 'gitbook-visitor-public-bucket',
+                value: JSON.stringify({ bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } } }),
+            },
+        ];
+        const url = new URL(
+            'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false&visitor.bucket.flags.HELLO=false'
+        );
+
+        const claims = getVisitorUnsignedClaims({ cookies, url });
+
+        expect(claims.all).toStrictEqual({
+            role: 'admin',
+            language: 'fr',
+            bucket: {
+                flags: { HELLO: false, SITE_AI: true, SITE_PREVIEW: true },
+            },
+            isEnterprise: true,
+            flags: {
+                ALPHA: true,
+                API: false,
+            },
+        });
+
+        expect(claims.fromVisitorParams).toStrictEqual({
+            bucket: {
+                flags: { HELLO: false },
+            },
+            isEnterprise: true,
+            flags: {
+                ALPHA: true,
+                API: false,
+            },
+        });
+    });
+});
diff --git a/packages/gitbook/src/lib/visitor-token.ts b/packages/gitbook/src/lib/visitors.ts
similarity index 61%
rename from packages/gitbook/src/lib/visitor-token.ts
rename to packages/gitbook/src/lib/visitors.ts
index 64f0c97931..7ae970666b 100644
--- a/packages/gitbook/src/lib/visitor-token.ts
+++ b/packages/gitbook/src/lib/visitors.ts
@@ -4,6 +4,7 @@ import hash from 'object-hash';
 
 const VISITOR_AUTH_PARAM = 'jwt_token';
 export const VISITOR_TOKEN_COOKIE = 'gitbook-visitor-token';
+const VISITOR_UNSIGNED_CLAIMS_PREFIX = 'gitbook-visitor-public';
 
 /**
  * Typing for a cookie, matching the internal type of Next.js.
@@ -30,6 +31,27 @@ type VisitorAuthCookieValue = {
     token: string;
 };
 
+type ClaimPrimitive =
+    | string
+    | number
+    | boolean
+    | null
+    | undefined
+    | { [key: string]: ClaimPrimitive }
+    | ClaimPrimitive[];
+
+/**
+ * The result of a visitor data lookup that can include:
+ *   - a visitor token (JWT)
+ *   - a record of visitor public/unsigned claims (JSON object)
+ *   - a session cookie response to persist any visitor query params across navigations.
+ */
+export type VisitorDataLookup = {
+    visitorToken: VisitorTokenLookup;
+    unsignedClaims: Record<string, ClaimPrimitive>;
+    visitorParamsCookie: ResponseCookie | undefined;
+};
+
 /**
  * The result of a visitor token lookup.
  */
@@ -53,6 +75,30 @@ export type VisitorTokenLookup =
     /** Not visitor token was found */
     | undefined;
 
+/**
+ * Get the visitor data for the request potentially including:
+ *   - a JWT token that may contain signed claims or can be used for VA authentication.
+ *   - a record of the unsigned claims passed via a cookie or visitor.* params.
+ *   - a session cookie response that is used to persist any visitor.* params that were passed via the site URL.
+ */
+export function getVisitorData({
+    cookies,
+    url,
+}: {
+    cookies: RequestCookies;
+    url: URL | NextRequest['nextUrl'];
+}): VisitorDataLookup {
+    const visitorToken = getVisitorToken({ cookies, url });
+    const unsignedClaims = getVisitorUnsignedClaims({ cookies, url });
+    const visitorParamsCookie = getResponseCookieForVisitorParams(unsignedClaims.fromVisitorParams);
+
+    return {
+        visitorToken,
+        unsignedClaims: unsignedClaims.all,
+        visitorParamsCookie,
+    };
+}
+
 /**
  * Get the visitor token for the request. This token can either be in the
  * query parameters or stored as a cookie.
@@ -82,6 +128,138 @@ export function getVisitorToken({
     }
 }
 
+/**
+ * Get the visitor unsigned/public claims for the request. They can either be in `visitor.` query
+ * parameters or stored in special `gitbook-visitor-public-*` cookies.
+ */
+export function getVisitorUnsignedClaims(args: {
+    cookies: RequestCookies;
+    url: URL | NextRequest['nextUrl'];
+}): {
+    /**
+     * The unsigned claims coming from both `gitbook-visitor-public` cookies and `visitor.*` query params.
+     */
+    all: Record<string, ClaimPrimitive>;
+    /**
+     * The unsigned claims from the `visitor.*` query params.
+     */
+    fromVisitorParams: Record<string, ClaimPrimitive>;
+} {
+    const { cookies, url } = args;
+    const claims: Record<string, ClaimPrimitive> = {};
+    const searchParamsClaims: Record<string, ClaimPrimitive> = {};
+
+    for (const cookie of cookies) {
+        if (cookie.name.startsWith(VISITOR_UNSIGNED_CLAIMS_PREFIX)) {
+            try {
+                const parsed = JSON.parse(cookie.value);
+                if (typeof parsed === 'object' && parsed !== null) {
+                    Object.assign(claims, parsed);
+                }
+            } catch (_err) {
+                console.warn(`Invalid JSON in unsigned claim cookie "${cookie.name}"`);
+            }
+        }
+    }
+
+    for (const [key, value] of url.searchParams.entries()) {
+        if (key.startsWith('visitor.')) {
+            const claimPath = key.substring('visitor.'.length);
+            const claimValue = parseVisitorQueryParamValue(value);
+
+            setVisitorClaimByPath(claims, claimPath, claimValue);
+            setVisitorClaimByPath(searchParamsClaims, claimPath, claimValue);
+        }
+    }
+
+    return { all: claims, fromVisitorParams: searchParamsClaims };
+}
+
+/**
+ * Set the value of claims in a claims object at a specific path.
+ */
+function setVisitorClaimByPath(
+    claims: Record<string, ClaimPrimitive>,
+    keyPath: string,
+    value: ClaimPrimitive
+): void {
+    const keys = keyPath.split('.');
+    let current = claims;
+
+    for (let index = 0; index < keys.length; index++) {
+        const key = keys[index];
+
+        if (index === keys.length - 1) {
+            current[key] = value;
+        } else {
+            if (!(key in current) || !isClaimPrimitiveObject(current[key])) {
+                current[key] = {};
+            }
+
+            current = current[key];
+        }
+    }
+}
+
+function isClaimPrimitiveObject(value: unknown): value is Record<string, ClaimPrimitive> {
+    return typeof value === 'object' && value !== null;
+}
+
+/**
+ * Parse the value expected in a `visitor.` URL query parameter.
+ */
+function parseVisitorQueryParamValue(value: string): ClaimPrimitive {
+    if (value === 'true') {
+        return true;
+    }
+
+    if (value === 'false') {
+        return false;
+    }
+
+    if (value === 'null') {
+        return null;
+    }
+
+    if (value === 'undefined') {
+        return undefined;
+    }
+
+    const num = Number(value);
+    if (!Number.isNaN(num) && value.trim() !== '') {
+        return num;
+    }
+
+    try {
+        const parsed = JSON.parse(value);
+        if (typeof parsed === 'object' && parsed !== null) {
+            return parsed;
+        }
+    } catch {}
+
+    return value;
+}
+
+/**
+ * Returns to cookie response to use in order to persist visitor params that were passed to the URL.
+ */
+function getResponseCookieForVisitorParams(
+    visitorParamsClaims: Record<string, ClaimPrimitive>
+): ResponseCookie | undefined {
+    if (Object.keys(visitorParamsClaims).length === 0) {
+        return undefined;
+    }
+
+    return {
+        name: VISITOR_UNSIGNED_CLAIMS_PREFIX,
+        value: JSON.stringify(visitorParamsClaims),
+        options: {
+            sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined,
+            secure: process.env.NODE_ENV === 'production',
+        },
+    };
+}
+
 /**
  * Return the lookup result for content served with visitor auth.
  */
diff --git a/packages/gitbook/src/lib/waitUntil.ts b/packages/gitbook/src/lib/waitUntil.ts
index 6e2940f17b..dbaae41a0d 100644
--- a/packages/gitbook/src/lib/waitUntil.ts
+++ b/packages/gitbook/src/lib/waitUntil.ts
@@ -1,4 +1,7 @@
 import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
+import { getCloudflareContext as getCloudflareContextV2 } from '@v2/lib/data/cloudflare';
+import { GITBOOK_RUNTIME } from '@v2/lib/env';
+import { isV2 } from './v2';
 
 let pendings: Array<Promise<unknown>> = [];
 
@@ -47,12 +50,25 @@ export async function waitUntil(promise: Promise<unknown>) {
         return;
     }
 
-    const cloudflareContext = await getGlobalContext();
-    if ('waitUntil' in cloudflareContext) {
-        cloudflareContext.waitUntil(promise);
-    } else {
-        await promise;
+    if (GITBOOK_RUNTIME === 'cloudflare') {
+        if (isV2()) {
+            const context = getCloudflareContextV2();
+            if (context) {
+                context.ctx.waitUntil(promise);
+                return;
+            }
+        } else {
+            const cloudflareContext = await getGlobalContext();
+            if ('waitUntil' in cloudflareContext) {
+                cloudflareContext.waitUntil(promise);
+                return;
+            }
+        }
     }
+
+    await promise.catch((error) => {
+        console.error('Ignored error in waitUntil', error);
+    });
 }
 
 /**
diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts
index 30f7b44e6a..9edb751243 100644
--- a/packages/gitbook/src/middleware.ts
+++ b/packages/gitbook/src/middleware.ts
@@ -24,9 +24,9 @@ import {
     type ResponseCookies,
     type VisitorTokenLookup,
     getResponseCookiesForVisitorAuth,
-    getVisitorToken,
+    getVisitorData,
     normalizeVisitorAuthURL,
-} from '@/lib/visitor-token';
+} from '@/lib/visitors';
 
 import { joinPath, withLeadingSlash } from '@/lib/paths';
 import { getProxyModeBasePath } from '@/lib/proxy';
@@ -201,7 +201,9 @@ export async function middleware(request: NextRequest) {
 
     const customization = url.searchParams.get('customization');
     if (customization && validateSerializedCustomization(customization)) {
-        headers.set(MiddlewareHeaders.Customization, customization);
+        // We need to encode the customization to ensure it is properly encoded in vercel.
+        // We do it here as well so that we have a single method to decode it later.
+        headers.set(MiddlewareHeaders.Customization, encodeURIComponent(customization));
     }
 
     const theme = url.searchParams.get('theme');
@@ -392,17 +394,17 @@ async function lookupSiteInProxy(request: NextRequest, url: URL): Promise<Lookup
  * When serving multi spaces based on the current URL.
  */
 async function lookupSiteInMultiMode(request: NextRequest, url: URL): Promise<LookupResult> {
-    const visitorAuthToken = getVisitorToken({
+    const { visitorToken } = getVisitorData({
         cookies: request.cookies.getAll(),
         url,
     });
-    const lookup = await lookupSiteByAPI(url, visitorAuthToken);
+    const lookup = await lookupSiteByAPI(url, visitorToken);
     return {
         ...lookup,
-        ...('basePath' in lookup && visitorAuthToken
-            ? getLookupResultForVisitorAuth(lookup.basePath, visitorAuthToken)
+        ...('basePath' in lookup && visitorToken
+            ? getLookupResultForVisitorAuth(lookup.basePath, visitorToken)
             : {}),
-        visitorToken: visitorAuthToken?.token,
+        visitorToken: visitorToken?.token,
     };
 }
 
@@ -609,12 +611,12 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis
     const target = new URL(targetStr);
     target.search = url.search;
 
-    const visitorAuthToken = getVisitorToken({
+    const { visitorToken } = getVisitorData({
         cookies: request.cookies.getAll(),
         url: target,
     });
 
-    const lookup = await lookupSiteByAPI(target, visitorAuthToken);
+    const lookup = await lookupSiteByAPI(target, visitorToken);
     if ('error' in lookup) {
         return lookup;
     }
@@ -641,10 +643,10 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis
         ...lookup,
         siteBasePath: joinPath(target.host, lookup.siteBasePath),
         basePath: joinPath(target.host, lookup.basePath),
-        ...('basePath' in lookup && visitorAuthToken
-            ? getLookupResultForVisitorAuth(lookup.basePath, visitorAuthToken)
+        ...('basePath' in lookup && visitorToken
+            ? getLookupResultForVisitorAuth(lookup.basePath, visitorToken)
             : {}),
-        visitorToken: visitorAuthToken?.token,
+        visitorToken: visitorToken?.token,
     };
 }
 
diff --git a/packages/gitbook/src/routes/icon.tsx b/packages/gitbook/src/routes/icon.tsx
index 03a6f35224..ab3429d631 100644
--- a/packages/gitbook/src/routes/icon.tsx
+++ b/packages/gitbook/src/routes/icon.tsx
@@ -24,6 +24,11 @@ const SIZES = {
     },
 };
 
+type RenderIconOptions = {
+    size: keyof typeof SIZES;
+    theme: 'light' | 'dark';
+};
+
 /**
  * Generate an icon for a site content.
  */
@@ -31,7 +36,7 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) {
     const options = getOptions(req.url);
     const size = SIZES[options.size];
 
-    const { site, customization } = context;
+    const { customization } = context;
     const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
 
     // If the site has a custom icon, redirect to it
@@ -45,17 +50,45 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) {
         );
     }
 
+    return new ImageResponse(<SiteDefaultIcon context={context} options={options} />, {
+        width: size.width,
+        height: size.height,
+        headers: {
+            'cache-tag': [
+                getCacheTag({
+                    tag: 'site',
+                    site: context.site.id,
+                }),
+            ].join(','),
+        },
+    });
+}
+
+/**
+ * Render the icon as a React node.
+ */
+export function SiteDefaultIcon(props: {
+    context: GitBookSiteContext;
+    options: RenderIconOptions;
+    style?: React.CSSProperties;
+    tw?: string;
+}) {
+    const { context, options, style, tw } = props;
+    const size = SIZES[options.size];
+
+    const { site, customization } = context;
     const contentTitle = site.title;
 
-    return new ImageResponse(
+    return (
         <div
-            tw={tcls(options.theme === 'light' ? 'bg-white' : 'bg-black', size.boxStyle)}
+            tw={tcls(options.theme === 'light' ? 'bg-white' : 'bg-black', size.boxStyle, tw)}
             style={{
                 width: '100%',
                 height: '100%',
                 display: 'flex',
                 alignItems: 'center',
                 justifyContent: 'center',
+                ...style,
             }}
         >
             <h2
@@ -70,19 +103,7 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) {
                     ? getEmojiForCode(customization.favicon.emoji)
                     : contentTitle.slice(0, 1).toUpperCase()}
             </h2>
-        </div>,
-        {
-            width: size.width,
-            height: size.height,
-            headers: {
-                'cache-tag': [
-                    getCacheTag({
-                        tag: 'site',
-                        site: context.site.id,
-                    }),
-                ].join(','),
-            },
-        }
+        </div>
     );
 }
 
diff --git a/packages/gitbook/src/routes/image.ts b/packages/gitbook/src/routes/image.ts
index 45d590f311..3bc946ed90 100644
--- a/packages/gitbook/src/routes/image.ts
+++ b/packages/gitbook/src/routes/image.ts
@@ -2,6 +2,7 @@ import {
     CURRENT_SIGNATURE_VERSION,
     type CloudflareImageOptions,
     type SignatureVersion,
+    SizableImageAction,
     checkIsSizableImageURL,
     isSignatureVersion,
     parseImageAPIURL,
@@ -40,7 +41,7 @@ export async function serveResizedImage(
     // Check again if the image can be sized, even though we checked when rendering the Image component
     // Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain
     // Also prevent infinite redirects.
-    if (!checkIsSizableImageURL(url)) {
+    if (checkIsSizableImageURL(url) === SizableImageAction.Skip) {
         return new Response('Invalid url parameter', { status: 400 });
     }
 
diff --git a/packages/gitbook/src/routes/llms-full.ts b/packages/gitbook/src/routes/llms-full.ts
new file mode 100644
index 0000000000..90efb73f63
--- /dev/null
+++ b/packages/gitbook/src/routes/llms-full.ts
@@ -0,0 +1,209 @@
+import path from 'node:path';
+import { joinPath } from '@/lib/paths';
+import { getIndexablePages } from '@/lib/sitemap';
+import { getSiteStructureSections } from '@/lib/sites';
+import { checkIsAnchor, checkIsExternalURL } from '@/lib/urls';
+import type { RevisionPageDocument, SiteSection, SiteSpace } from '@gitbook/api';
+import { type GitBookSiteContext, checkIsRootSiteContext } from '@v2/lib/context';
+import { throwIfDataError } from '@v2/lib/data';
+import assertNever from 'assert-never';
+import type { Link, Paragraph, Root } from 'mdast';
+import { fromMarkdown } from 'mdast-util-from-markdown';
+import { frontmatterFromMarkdown } from 'mdast-util-frontmatter';
+import { gfmFromMarkdown, gfmToMarkdown } from 'mdast-util-gfm';
+import { toMarkdown } from 'mdast-util-to-markdown';
+import { frontmatter } from 'micromark-extension-frontmatter';
+import { gfm } from 'micromark-extension-gfm';
+import { pMapIterable } from 'p-map';
+import { remove } from 'unist-util-remove';
+import { visit } from 'unist-util-visit';
+
+// We limit the concurrency to 100 to avoid reaching limit with concurrent requests
+// or file descriptor limits.
+const MAX_CONCURRENCY = 100;
+
+/**
+ * Generate a llms-full.txt file for the site.
+ * As the result can be large, we stream it as we generate it.
+ */
+export async function serveLLMsFullTxt(context: GitBookSiteContext) {
+    if (!checkIsRootSiteContext(context)) {
+        return new Response('llms.txt is only served from the root of the site', { status: 404 });
+    }
+
+    return new Response(
+        new ReadableStream<Uint8Array>({
+            async pull(controller) {
+                await streamMarkdownFromSiteStructure(context, controller);
+                controller.close();
+            },
+        }),
+        {
+            headers: {
+                'Content-Type': 'text/markdown; charset=utf-8',
+            },
+        }
+    );
+}
+
+/**
+ * Stream markdown from site structure.
+ */
+async function streamMarkdownFromSiteStructure(
+    context: GitBookSiteContext,
+    stream: ReadableStreamDefaultController<Uint8Array>
+): Promise<void> {
+    switch (context.structure.type) {
+        case 'sections':
+            return streamMarkdownFromSections(
+                context,
+                stream,
+                getSiteStructureSections(context.structure, { ignoreGroups: true })
+            );
+        case 'siteSpaces':
+            return streamMarkdownFromSiteSpaces(context, stream, context.structure.structure, '');
+        default:
+            assertNever(context.structure);
+    }
+}
+
+/**
+ * Stream markdown from site sections.
+ */
+async function streamMarkdownFromSections(
+    context: GitBookSiteContext,
+    stream: ReadableStreamDefaultController<Uint8Array>,
+    siteSections: SiteSection[]
+): Promise<void> {
+    for (const siteSection of siteSections) {
+        await streamMarkdownFromSiteSpaces(
+            context,
+            stream,
+            siteSection.siteSpaces,
+            siteSection.path
+        );
+    }
+}
+
+/**
+ * Stream markdown from site spaces.
+ */
+async function streamMarkdownFromSiteSpaces(
+    context: GitBookSiteContext,
+    stream: ReadableStreamDefaultController<Uint8Array>,
+    siteSpaces: SiteSpace[],
+    basePath: string
+): Promise<void> {
+    const { dataFetcher } = context;
+
+    for (const siteSpace of siteSpaces) {
+        const siteSpaceUrl = siteSpace.urls.published;
+        if (!siteSpaceUrl) {
+            continue;
+        }
+        const rootPages = await throwIfDataError(
+            dataFetcher.getRevisionPages({
+                spaceId: siteSpace.space.id,
+                revisionId: siteSpace.space.revision,
+                metadata: false,
+            })
+        );
+        const pages = getIndexablePages(rootPages);
+
+        for await (const markdown of pMapIterable(
+            pages,
+            async ({ page }) => {
+                if (page.type !== 'document') {
+                    return '';
+                }
+
+                return getMarkdownForPage(
+                    context,
+                    siteSpace,
+                    page,
+                    joinPath(basePath, siteSpace.path)
+                );
+            },
+            {
+                concurrency: MAX_CONCURRENCY,
+            }
+        )) {
+            stream.enqueue(new TextEncoder().encode(markdown));
+        }
+    }
+}
+
+/**
+ * Get markdown from a page.
+ */
+async function getMarkdownForPage(
+    context: GitBookSiteContext,
+    siteSpace: SiteSpace,
+    page: RevisionPageDocument,
+    basePath: string
+): Promise<string> {
+    const { dataFetcher } = context;
+
+    const pageMarkdown = await throwIfDataError(
+        dataFetcher.getRevisionPageMarkdown({
+            spaceId: siteSpace.space.id,
+            revisionId: siteSpace.space.revision,
+            pageId: page.id,
+        })
+    );
+
+    const tree = fromMarkdown(pageMarkdown, {
+        extensions: [frontmatter(['yaml']), gfm()],
+        mdastExtensions: [frontmatterFromMarkdown(['yaml']), gfmFromMarkdown()],
+    });
+
+    // Remove frontmatter
+    remove(tree, 'yaml');
+
+    if (page.description) {
+        // The first node is the page title as a H1, we insert the description as a paragraph
+        // after it.
+        const descriptionNode: Paragraph = {
+            type: 'paragraph',
+            children: [{ type: 'text', value: page.description }],
+        };
+        tree.children.splice(1, 0, descriptionNode);
+    }
+
+    // Rewrite relative links to absolute links
+    transformLinks(context, tree, { currentPagePath: page.path, basePath });
+
+    const markdown = toMarkdown(tree, { extensions: [gfmToMarkdown()] });
+    return `${markdown}\n\n`;
+}
+
+/**
+ * Re-writes the URL of every relative <a> link so it is expressed from the site-root.
+ */
+export function transformLinks(
+    context: GitBookSiteContext,
+    tree: Root,
+    options: { currentPagePath: string; basePath: string }
+): Root {
+    const { linker } = context;
+    const { currentPagePath, basePath } = options;
+    const currentDir = path.posix.dirname(currentPagePath);
+
+    visit(tree, 'link', (node: Link) => {
+        const original = node.url;
+
+        // Skip anchors, mailto:, http(s):, protocol-like, or already-rooted paths
+        if (checkIsExternalURL(original) || checkIsAnchor(original) || original.startsWith('/')) {
+            return;
+        }
+
+        // Resolve against the current page’s directory and strip any leading “/”
+        const pathInSite = path.posix
+            .normalize(path.posix.join(basePath, currentDir, original))
+            .replace(/^\/+/, '');
+
+        node.url = linker.toPathInSite(pathInSite);
+    });
+
+    return tree;
+}
diff --git a/packages/gitbook/src/routes/llms.ts b/packages/gitbook/src/routes/llms.ts
index 24f9119505..a7e953cbad 100644
--- a/packages/gitbook/src/routes/llms.ts
+++ b/packages/gitbook/src/routes/llms.ts
@@ -46,7 +46,7 @@ export async function serveLLMsTxt(
         }),
         {
             headers: {
-                'Content-Type': 'text/plain; charset=utf-8',
+                'Content-Type': 'text/markdown; charset=utf-8',
             },
         }
     );
diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx
index 41842ab241..4d8a83b2c3 100644
--- a/packages/gitbook/src/routes/ogimage.tsx
+++ b/packages/gitbook/src/routes/ogimage.tsx
@@ -1,41 +1,33 @@
 import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api';
 import { colorContrast } from '@gitbook/colors';
+import { type FontWeight, getDefaultFont } from '@gitbook/fonts';
+import { direction } from 'direction';
+import { imageSize } from 'image-size';
 import { redirect } from 'next/navigation';
 import { ImageResponse } from 'next/og';
 
 import { type PageParams, fetchPageData } from '@/components/SitePage';
 import { getFontSourcesToPreload } from '@/fonts/custom';
 import { getAssetURL } from '@/lib/assets';
+import { getExtension } from '@/lib/paths';
 import { filterOutNullable } from '@/lib/typescript';
 import { getCacheTag } from '@gitbook/cache-tags';
 import type { GitBookSiteContext } from '@v2/lib/context';
-import { getCloudflareContext } from '@v2/lib/data/cloudflare';
-import { getResizedImageURL } from '@v2/lib/images';
-
-const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
-    [CustomizationDefaultFont.Inter]: 'Inter',
-    [CustomizationDefaultFont.FiraSans]: 'Fira Sans Extra Condensed',
-    [CustomizationDefaultFont.IBMPlexSerif]: 'IBM Plex Serif',
-    [CustomizationDefaultFont.Lato]: 'Lato',
-    [CustomizationDefaultFont.Merriweather]: 'Merriweather',
-    [CustomizationDefaultFont.NotoSans]: 'Noto Sans',
-    [CustomizationDefaultFont.OpenSans]: 'Open Sans',
-    [CustomizationDefaultFont.Overpass]: 'Overpass',
-    [CustomizationDefaultFont.Poppins]: 'Poppins',
-    [CustomizationDefaultFont.Raleway]: 'Raleway',
-    [CustomizationDefaultFont.Roboto]: 'Roboto',
-    [CustomizationDefaultFont.RobotoSlab]: 'Roboto Slab',
-    [CustomizationDefaultFont.SourceSansPro]: 'Source Sans 3',
-    [CustomizationDefaultFont.Ubuntu]: 'Ubuntu',
-    [CustomizationDefaultFont.ABCFavorit]: 'Inter',
-};
+import {
+    type ResizeImageOptions,
+    SizableImageAction,
+    checkIsSizableImageURL,
+    getResizedImageURL,
+    resizeImage,
+} from '@v2/lib/images';
+import { SiteDefaultIcon } from './icon';
 
 /**
  * Render the OpenGraph image for a site content.
  */
 export async function serveOGImage(baseContext: GitBookSiteContext, params: PageParams) {
     const { context, pageTarget } = await fetchPageData(baseContext, params);
-    const { customization, site, linker, imageResizer } = context;
+    const { customization, site, imageResizer } = context;
     const page = pageTarget?.page;
 
     // If user configured a custom social preview, we redirect to it.
@@ -49,7 +41,6 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
     }
 
     // Compute all text to load only the necessary fonts
-    const contentTitle = customization.header.logo ? '' : site.title;
     const pageTitle = page
         ? page.title.length > 64
             ? `${page.title.slice(0, 64)}...`
@@ -63,18 +54,18 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
             : '';
 
     // Load the fonts
-    const { fontFamily, fonts } = await (async () => {
+    const fontLoader = async () => {
         // google fonts
         if (typeof customization.styling.font === 'string') {
-            const fontFamily = googleFontsMap[customization.styling.font] ?? 'Inter';
+            const fontFamily = customization.styling.font ?? CustomizationDefaultFont.Inter;
 
             const regularText = pageDescription;
-            const boldText = `${contentTitle}${pageTitle}`;
+            const boldText = `${site.title} ${pageTitle}`;
 
             const fonts = (
                 await Promise.all([
-                    loadGoogleFont({ fontFamily, text: regularText, weight: 400 }),
-                    loadGoogleFont({ fontFamily, text: boldText, weight: 700 }),
+                    loadGoogleFont({ font: fontFamily, text: regularText, weight: 400 }),
+                    loadGoogleFont({ font: fontFamily, text: boldText, weight: 700 }),
                 ])
             ).filter(filterOutNullable);
 
@@ -103,7 +94,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
         ).filter(filterOutNullable);
 
         return { fontFamily: 'CustomFont', fonts };
-    })();
+    };
 
     const theme = customization.themes.default;
     const useLightTheme = theme === 'light';
@@ -157,32 +148,55 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
             break;
     }
 
-    const favicon = await (async () => {
-        if ('icon' in customization.favicon)
-            return (
-                <img
-                    src={customization.favicon.icon[theme]}
-                    width={40}
-                    height={40}
-                    tw="mr-4"
-                    alt="Icon"
-                />
-            );
-        if ('emoji' in customization.favicon)
-            return (
-                <span tw="text-4xl mr-4">
-                    {String.fromCodePoint(Number.parseInt(`0x${customization.favicon.emoji}`))}
-                </span>
-            );
-        const src = await readSelfImage(
-            linker.toAbsoluteURL(
-                linker.toPathInSpace(
-                    `~gitbook/icon?size=medium&theme=${customization.themes.default}`
-                )
-            )
+    const faviconLoader = async () => {
+        if (customization.header.logo) {
+            // Don't load the favicon if we have a logo
+            // as it'll not be used.
+            return null;
+        }
+
+        const faviconSize = {
+            width: 48,
+            height: 48,
+        };
+
+        if ('icon' in customization.favicon) {
+            const faviconImage = await fetchImage(customization.favicon.icon[theme], faviconSize);
+            if (faviconImage) {
+                return <img {...faviconImage} {...faviconSize} alt="Icon" />;
+            }
+        }
+
+        return (
+            <SiteDefaultIcon
+                context={context}
+                options={{
+                    size: 'small',
+                    theme,
+                }}
+                style={faviconSize}
+            />
         );
-        return <img src={src} alt="Icon" width={40} height={40} tw="mr-4" />;
-    })();
+    };
+
+    const logoLoader = async () => {
+        if (!customization.header.logo) {
+            return null;
+        }
+
+        return await fetchImage(
+            useLightTheme ? customization.header.logo.light : customization.header.logo.dark,
+            {
+                height: 60,
+            }
+        );
+    };
+
+    const [favicon, logo, { fontFamily, fonts }] = await Promise.all([
+        faviconLoader(),
+        logoLoader(),
+        fontLoader(),
+    ]);
 
     return new ImageResponse(
         <div
@@ -203,25 +217,19 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
             {/* Grid */}
             <img
                 tw="absolute inset-0 w-[100vw] h-[100vh]"
-                src={await readStaticImage(gridAsset)}
+                src={(await fetchStaticImage(gridAsset)).src}
                 alt="Grid"
             />
 
             {/* Logo */}
-            {customization.header.logo ? (
-                <img
-                    alt="Logo"
-                    height={60}
-                    src={
-                        useLightTheme
-                            ? customization.header.logo.light
-                            : customization.header.logo.dark
-                    }
-                />
+            {logo ? (
+                <div tw="flex flex-row">
+                    <img {...logo} alt="Logo" tw="h-[60px]" />
+                </div>
             ) : (
-                <div tw="flex">
+                <div tw="flex flex-row items-center">
                     {favicon}
-                    <h3 tw="text-4xl my-0 font-bold">{contentTitle}</h3>
+                    <h3 tw="text-4xl ml-4 my-0 font-bold">{transformText(site.title)}</h3>
                 </div>
             )}
 
@@ -230,10 +238,12 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
                 <h1
                     tw={`text-8xl my-0 tracking-tight leading-none text-left text-[${colors.title}] font-bold`}
                 >
-                    {pageTitle}
+                    {transformText(pageTitle)}
                 </h1>
                 {pageDescription ? (
-                    <h2 tw="text-4xl mb-0 mt-8 w-[75%] font-normal">{pageDescription}</h2>
+                    <h2 tw="text-4xl mb-0 mt-8 w-[75%] font-normal">
+                        {transformText(pageDescription)}
+                    </h2>
                 ) : null}
             </div>
         </div>,
@@ -257,37 +267,31 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
     );
 }
 
-async function loadGoogleFont(input: { fontFamily: string; text: string; weight: 400 | 700 }) {
-    const { fontFamily, text, weight } = input;
-
-    if (!text.trim()) {
-        return null;
-    }
-
-    const url = new URL('https://fonts.googleapis.com/css2');
-    url.searchParams.set('family', `${fontFamily}:wght@${weight}`);
-    url.searchParams.set('text', text);
-
-    const result = await fetch(url.href);
-    if (!result.ok) {
-        return null;
-    }
-
-    const css = await result.text();
-    const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
-    const resourceUrl = resource ? resource[1] : null;
-
-    if (resourceUrl) {
-        const response = await fetch(resourceUrl);
-        if (response.ok) {
-            const data = await response.arrayBuffer();
-            return {
-                name: fontFamily,
-                data,
-                style: 'normal' as const,
-                weight,
-            };
-        }
+async function loadGoogleFont(input: {
+    font: CustomizationDefaultFont;
+    text: string;
+    weight: FontWeight;
+}) {
+    const lookup = getDefaultFont({
+        font: input.font,
+        text: input.text,
+        weight: input.weight,
+    });
+
+    // If we found a font file, load it
+    if (lookup) {
+        return getWithCache(`google-font-files:${lookup.url}`, async () => {
+            const response = await fetch(lookup.url);
+            if (response.ok) {
+                const data = await response.arrayBuffer();
+                return {
+                    name: lookup.font,
+                    data,
+                    style: 'normal' as const,
+                    weight: input.weight,
+                };
+            }
+        });
     }
 
     // If for some reason we can't load the font, we'll just use the default one
@@ -311,58 +315,100 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
     };
 }
 
+// biome-ignore lint/suspicious/noExplicitAny: <explanation>
+const staticCache = new Map<string, any>();
+
 /**
- * Fetch a resource from the function itself.
- * To avoid error with worker to worker requests in the same zone, we use the `WORKER_SELF_REFERENCE` binding.
+ * Get or initialize a value in the static cache.
  */
-async function fetchSelf(url: string) {
-    const cloudflare = getCloudflareContext();
-    if (cloudflare?.env.WORKER_SELF_REFERENCE) {
-        return await cloudflare.env.WORKER_SELF_REFERENCE.fetch(
-            // `getAssetURL` can return a relative URL, so we need to make it absolute
-            // the URL doesn't matter, as we're using the worker-self-reference binding
-            new URL(url, 'https://worker-self-reference/')
-        );
+async function getWithCache<T>(key: string, fn: () => Promise<T>) {
+    const cached = staticCache.get(key) as T;
+    if (cached) {
+        return Promise.resolve(cached);
     }
 
-    return await fetch(url);
+    const result = await fn();
+    staticCache.set(key, result);
+    return result;
 }
 
 /**
- * Read an image from a response as a base64 encoded string.
+ * Read a static image and cache it in memory.
  */
-async function readImage(response: Response) {
-    const contentType = response.headers.get('content-type');
-    if (!contentType || !contentType.startsWith('image/')) {
-        throw new Error(`Invalid content type: ${contentType}`);
-    }
+async function fetchStaticImage(url: string) {
+    return getWithCache(`static-image:${url}`, async () => {
+        const image = await fetchImage(url);
+        if (!image) {
+            throw new Error('Failed to fetch static image');
+        }
 
-    const arrayBuffer = await response.arrayBuffer();
-    const base64 = Buffer.from(arrayBuffer).toString('base64');
-    return `data:${contentType};base64,${base64}`;
+        return image;
+    });
 }
 
-const staticImagesCache = new Map<string, string>();
+/**
+ * @vercel/og supports the following image formats:
+ * Extracted from https://github.com/vercel/next.js/blob/canary/packages/next/src/compiled/%40vercel/og/index.node.js
+ */
+const UNSUPPORTED_IMAGE_EXTENSIONS = ['.avif', '.webp'];
+const SUPPORTED_IMAGE_TYPES = [
+    'image/png',
+    'image/apng',
+    'image/jpeg',
+    'image/gif',
+    'image/svg+xml',
+];
 
 /**
- * Read a static image and cache it in memory.
+ * Fetch an image from a URL and return a base64 encoded string.
+ * We do this as @vercel/og is otherwise failing on SVG images referenced by a URL.
  */
-async function readStaticImage(url: string) {
-    const cached = staticImagesCache.get(url);
-    if (cached) {
-        return cached;
+async function fetchImage(url: string, options?: ResizeImageOptions) {
+    // Skip early some images to avoid fetching them
+    const parsedURL = new URL(url);
+    if (UNSUPPORTED_IMAGE_EXTENSIONS.includes(getExtension(parsedURL.pathname).toLowerCase())) {
+        return null;
+    }
+
+    // We use the image resizer to normalize the image format to PNG.
+    // as @vercel/og can sometimes fail on some JPEG images.
+    const response =
+        checkIsSizableImageURL(url) !== SizableImageAction.Resize
+            ? await fetch(url)
+            : await resizeImage(url, {
+                  ...options,
+                  format: 'png',
+              });
+
+    // Filter out unsupported image types
+    const contentType = response.headers.get('content-type');
+    if (!contentType || !SUPPORTED_IMAGE_TYPES.some((type) => contentType.includes(type))) {
+        return null;
     }
 
-    const image = await readSelfImage(url);
-    staticImagesCache.set(url, image);
-    return image;
+    const arrayBuffer = await response.arrayBuffer();
+    const buffer = Buffer.from(arrayBuffer);
+    const base64 = buffer.toString('base64');
+    const src = `data:${contentType};base64,${base64}`;
+
+    try {
+        const { width, height } = imageSize(buffer);
+        return { src, width, height };
+    } catch {
+        return null;
+    }
 }
 
 /**
- * Read an image from GitBook itself.
+ * @vercel/og doesn't support RTL text, so we need to transform with a HACK for now.
+ * We can remove it once support has been added.
+ * https://github.com/vercel/satori/issues/74
  */
-async function readSelfImage(url: string) {
-    const response = await fetchSelf(url);
-    const image = await readImage(response);
-    return image;
+function transformText(text: string) {
+    const dir = direction(text);
+    if (dir !== 'rtl') {
+        return text;
+    }
+
+    return '';
 }
diff --git a/packages/gitbook/src/routes/robots.ts b/packages/gitbook/src/routes/robots.ts
index 297fc46771..1e1c0a2873 100644
--- a/packages/gitbook/src/routes/robots.ts
+++ b/packages/gitbook/src/routes/robots.ts
@@ -8,20 +8,23 @@ export async function serveRobotsTxt(context: GitBookSiteContext) {
     const { linker } = context;
 
     const isRoot = checkIsRootSiteContext(context);
-    const lines = [
-        'User-agent: *',
-        // Disallow dynamic routes / search queries
-        'Disallow: /*?',
-        ...((await isSiteIndexable(context))
-            ? [
-                  'Allow: /',
-                  `Sitemap: ${linker.toAbsoluteURL(linker.toPathInSpace(isRoot ? '/sitemap.xml' : '/sitemap-pages.xml'))}`,
-              ]
-            : ['Disallow: /']),
-    ];
-    const content = lines.join('\n');
+    const isIndexable = await isSiteIndexable(context);
 
-    return new Response(content, {
+    const lines = isIndexable
+        ? [
+              'User-agent: *',
+              // Allow image resizing and icon generation routes for favicons and search results
+              'Allow: /~gitbook/image?*',
+              'Allow: /~gitbook/icon?*',
+              // Disallow other dynamic routes / search queries
+              'Disallow: /*?',
+              'Allow: /',
+              `Sitemap: ${linker.toAbsoluteURL(linker.toPathInSpace(isRoot ? '/sitemap.xml' : '/sitemap-pages.xml'))}`,
+          ]
+        : ['User-agent: *', 'Disallow: /'];
+
+    const robotsTxt = lines.join('\n');
+    return new Response(robotsTxt, {
         headers: {
             'Content-Type': 'text/plain',
         },
diff --git a/packages/gitbook/src/routes/sitemap.ts b/packages/gitbook/src/routes/sitemap.ts
index 4a64e12387..a5f5e452fb 100644
--- a/packages/gitbook/src/routes/sitemap.ts
+++ b/packages/gitbook/src/routes/sitemap.ts
@@ -141,7 +141,7 @@ function getUrlsFromSiteSpaces(context: GitBookSiteContext, siteSpaces: SiteSpac
         }
         const url = new URL(siteSpace.urls.published);
         url.pathname = joinPath(url.pathname, 'sitemap-pages.xml');
-        return context.linker.toLinkForContent(url.toString());
+        return context.linker.toAbsoluteURL(context.linker.toLinkForContent(url.toString()));
     }, []);
     return urls.filter(filterOutNullable);
 }
diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts
index 7a05bd15fc..192ad39eb8 100644
--- a/packages/gitbook/tailwind.config.ts
+++ b/packages/gitbook/tailwind.config.ts
@@ -443,9 +443,17 @@ const config: Config = {
             scale: {
                 '98': '0.98',
                 '102': '1.02',
+                '104': '1.04',
             },
         },
         opacity: opacity(),
+        screens: {
+            sm: '640px',
+            md: '768px',
+            lg: '1024px',
+            xl: '1280px',
+            '2xl': '1536px',
+        },
     },
     plugins: [
         plugin(({ addVariant }) => {
@@ -457,12 +465,16 @@ const config: Config = {
             /**
              * Variant when a header is displayed.
              */
-            addVariant('site-header-none', 'html.site-header-none &');
+            addVariant('site-header-none', 'body:not(:has(#site-header:not(.mobile-only))) &');
             addVariant('site-header', 'body:has(#site-header:not(.mobile-only)) &');
             addVariant('site-header-sections', [
                 'body:has(#site-header:not(.mobile-only) #sections) &',
                 'body:has(.page-no-toc):has(#site-header:not(.mobile-only) #variants) &',
             ]);
+            addVariant(
+                'announcement',
+                'html:not(.announcement-hidden):has(#announcement-banner) &'
+            );
 
             const customisationVariants = {
                 // Sidebar styles
@@ -478,7 +490,10 @@ const config: Config = {
                 theme: ['theme-clean', 'theme-muted', 'theme-bold', 'theme-gradient'],
 
                 // Corner styles
-                corner: ['straight-corners'],
+                corner: ['straight-corners', 'rounded-corners', 'circular-corners'],
+
+                // Depth styles
+                depth: ['depth-flat', 'depth-subtle'],
 
                 // Link styles
                 links: ['links-default', 'links-accent'],
@@ -506,8 +521,9 @@ const config: Config = {
             /**
              * Variant when the page contains a block that will be rendered in full-width mode.
              */
+            addVariant('site-full-width', 'body:has(.site-full-width) &');
+            addVariant('site-default-width', 'body:has(.site-default-width) &');
             addVariant('page-full-width', 'body:has(.page-full-width) &');
-            addVariant('page-default-width', 'body:has(.page-default-width) &');
 
             /**
              * Variant when the page is configured to hide the table of content.
diff --git a/packages/gitbook/tests/pagespeed-testing.ts b/packages/gitbook/tests/pagespeed-testing.ts
index 94f48d4201..07f1f01b3d 100644
--- a/packages/gitbook/tests/pagespeed-testing.ts
+++ b/packages/gitbook/tests/pagespeed-testing.ts
@@ -12,13 +12,13 @@ interface Test {
 // and to be able to see the results, and only catch major regressions.
 const tests: Array<Test> = [
     {
-        url: 'https://docs.gitbook.com',
+        url: 'https://gitbook.com/docs',
         strategy: 'desktop',
         threshold: 60,
     },
 
     {
-        url: 'https://docs.gitbook.com',
+        url: 'https://gitbook.com/docs',
         strategy: 'mobile',
         threshold: 30,
     },
diff --git a/packages/gitbook/tests/preload-bun.ts b/packages/gitbook/tests/preload-bun.ts
new file mode 100644
index 0000000000..3fc846ae26
--- /dev/null
+++ b/packages/gitbook/tests/preload-bun.ts
@@ -0,0 +1,8 @@
+import { mock } from 'bun:test';
+
+/**
+ * Mock the `server-only` module to avoid errors when running tests as it doesn't work well in Bun
+ */
+mock.module('server-only', () => {
+    return {};
+});
diff --git a/packages/icons/package.json b/packages/icons/package.json
index 472b7a7b3b..0e0b802123 100644
--- a/packages/icons/package.json
+++ b/packages/icons/package.json
@@ -32,7 +32,7 @@
     },
     "scripts": {
         "generate": "bun ./bin/gen-list.js",
-        "build": "tsc",
+        "build": "tsc --project tsconfig.build.json",
         "typecheck": "tsc --noEmit",
         "dev": "tsc -w",
         "clean": "rm -rf ./dist && rm -rf ./src/data",
@@ -41,7 +41,7 @@
     "bin": {
         "gitbook-icons": "./bin/gitbook-icons.js"
     },
-    "files": ["dist", "src", "bin", "data", "README.md", "CHANGELOG.md"],
+    "files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
     "engines": {
         "node": ">=20.0.0"
     }
diff --git a/packages/icons/tsconfig.build.json b/packages/icons/tsconfig.build.json
new file mode 100644
index 0000000000..e4828bc1f6
--- /dev/null
+++ b/packages/icons/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+    "extends": "./tsconfig.json",
+    "exclude": ["node_modules", "src/**/*.test.ts"]
+}
diff --git a/packages/openapi-parser/CHANGELOG.md b/packages/openapi-parser/CHANGELOG.md
index 36f5cbc224..7f6ac7ccd2 100644
--- a/packages/openapi-parser/CHANGELOG.md
+++ b/packages/openapi-parser/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @gitbook/openapi-parser
 
+## 2.1.4
+
+### Patch Changes
+
+- d00dc8c: Pass scalar's errors through OpenAPIParseError
+
 ## 2.1.3
 
 ### Patch Changes
diff --git a/packages/openapi-parser/package.json b/packages/openapi-parser/package.json
index f0829fca1b..e65ebb4dda 100644
--- a/packages/openapi-parser/package.json
+++ b/packages/openapi-parser/package.json
@@ -9,7 +9,7 @@
             "default": "./dist/index.js"
         }
     },
-    "version": "2.1.3",
+    "version": "2.1.4",
     "sideEffects": false,
     "dependencies": {
         "@scalar/openapi-parser": "^0.10.10",
diff --git a/packages/openapi-parser/src/error.ts b/packages/openapi-parser/src/error.ts
index e0d25d3e29..a7c5398bcb 100644
--- a/packages/openapi-parser/src/error.ts
+++ b/packages/openapi-parser/src/error.ts
@@ -1,3 +1,5 @@
+import type { ErrorObject } from '@scalar/openapi-parser';
+
 type OpenAPIParseErrorCode =
     | 'invalid'
     | 'parse-v2-in-v3'
@@ -12,17 +14,19 @@ export class OpenAPIParseError extends Error {
     public override name = 'OpenAPIParseError';
     public code: OpenAPIParseErrorCode;
     public rootURL: string | null;
-
+    public errors: ErrorObject[] | undefined;
     constructor(
         message: string,
         options: {
             code: OpenAPIParseErrorCode;
             rootURL?: string | null;
             cause?: Error;
+            errors?: ErrorObject[] | undefined;
         }
     ) {
         super(message, { cause: options.cause });
         this.code = options.code;
         this.rootURL = options.rootURL ?? null;
+        this.errors = options.errors;
     }
 }
diff --git a/packages/openapi-parser/src/v3.ts b/packages/openapi-parser/src/v3.ts
index 14a890769a..3b54819cff 100644
--- a/packages/openapi-parser/src/v3.ts
+++ b/packages/openapi-parser/src/v3.ts
@@ -40,6 +40,7 @@ async function untrustedValidate(input: ValidateOpenAPIV3Input) {
         throw new OpenAPIParseError('Invalid OpenAPI document', {
             code: 'invalid',
             rootURL,
+            errors: result.errors,
         });
     }
 
diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json
index d2c8bd8ee7..af2f568544 100644
--- a/packages/react-contentkit/package.json
+++ b/packages/react-contentkit/package.json
@@ -10,7 +10,7 @@
     },
     "dependencies": {
         "classnames": "^2.5.1",
-        "@gitbook/api": "*",
+        "@gitbook/api": "catalog:",
         "@gitbook/icons": "workspace:*"
     },
     "peerDependencies": {
diff --git a/packages/react-contentkit/src/ContentKit.tsx b/packages/react-contentkit/src/ContentKit.tsx
index 9d53c70de2..4f413f367a 100644
--- a/packages/react-contentkit/src/ContentKit.tsx
+++ b/packages/react-contentkit/src/ContentKit.tsx
@@ -38,10 +38,18 @@ export function ContentKit<RenderContext>(props: {
     render: (input: {
         renderContext: RenderContext;
         request: RequestRenderIntegrationUI;
-    }) => Promise<{
-        children: React.ReactNode;
-        output: ContentKitRenderOutput;
-    }>;
+    }) => Promise<
+        | {
+              error?: undefined;
+              children: React.ReactNode;
+              output: ContentKitRenderOutput;
+          }
+        | {
+              error: string;
+              children?: undefined;
+              output?: undefined;
+          }
+    >;
     /** Callback when an action is triggered */
     onAction?: (action: ContentKitAction) => void;
     /** Callback when the flow is completed */
@@ -98,17 +106,18 @@ export function ContentKit<RenderContext>(props: {
                 request: newInput,
             });
             const output = result.output;
+            if (output) {
+                if (output.type === 'complete') {
+                    return onComplete?.(output.returnValue);
+                }
 
-            if (output.type === 'complete') {
-                return onComplete?.(output.returnValue);
+                setCurrent((prev) => ({
+                    input: newInput,
+                    children: result.children,
+                    output: output,
+                    state: prev.state,
+                }));
             }
-
-            setCurrent((prev) => ({
-                input: newInput,
-                children: result.children,
-                output: output,
-                state: prev.state,
-            }));
         },
         [setCurrent, current, render, onComplete]
     );
@@ -147,8 +156,10 @@ export function ContentKit<RenderContext>(props: {
                             renderContext,
                             request: modalInput,
                         });
-
-                        if (result.output.type === 'element' || !result.output.type) {
+                        if (
+                            result.output &&
+                            (result.output.type === 'element' || !result.output.type)
+                        ) {
                             setSubView({
                                 mode: 'modal',
                                 initialInput: modalInput,
diff --git a/packages/react-openapi/CHANGELOG.md b/packages/react-openapi/CHANGELOG.md
index f74c6f044b..6f36c7dc61 100644
--- a/packages/react-openapi/CHANGELOG.md
+++ b/packages/react-openapi/CHANGELOG.md
@@ -1,5 +1,24 @@
 # @gitbook/react-openapi
 
+## 1.3.0
+
+### Minor Changes
+
+- 326e28e: Design tweaks to code blocks and OpenAPI pages
+
+### Patch Changes
+
+- 42ca7e1: Fix openapi CR preview
+- 5e975ab: Fix code highlighting for HTTP
+- 580101d: Fix schemas disclosure label causing client error
+- 20ebecb: Missing top-level required OpenAPI alternatives
+- 80cb52a: Handle OpenAPI alternatives from schema.items
+- cb5598d: Handle invalid OpenAPI Responses
+- c6637b0: Use default value if string number or boolean in generateSchemaExample
+- a3ec264: Fix Python code sample "null vs None"
+- Updated dependencies [d00dc8c]
+  - @gitbook/openapi-parser@2.1.4
+
 ## 1.2.1
 
 ### Patch Changes
diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json
index cd1099c242..d4f170270a 100644
--- a/packages/react-openapi/package.json
+++ b/packages/react-openapi/package.json
@@ -8,7 +8,7 @@
             "default": "./dist/index.js"
         }
     },
-    "version": "1.2.1",
+    "version": "1.3.0",
     "sideEffects": false,
     "dependencies": {
         "@gitbook/openapi-parser": "workspace:*",
diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx
index 8b67bbedc4..8bffb6d9ad 100644
--- a/packages/react-openapi/src/OpenAPICodeSample.tsx
+++ b/packages/react-openapi/src/OpenAPICodeSample.tsx
@@ -312,6 +312,11 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
                 [name]: 'YOUR_API_KEY',
             };
         }
+        case 'oauth2': {
+            return {
+                Authorization: 'Bearer YOUR_OAUTH2_TOKEN',
+            };
+        }
         default: {
             return {};
         }
diff --git a/packages/react-openapi/src/OpenAPIDisclosure.tsx b/packages/react-openapi/src/OpenAPIDisclosure.tsx
index d84b4523ba..56e663b44b 100644
--- a/packages/react-openapi/src/OpenAPIDisclosure.tsx
+++ b/packages/react-openapi/src/OpenAPIDisclosure.tsx
@@ -9,11 +9,12 @@ import { Button, Disclosure, DisclosurePanel } from 'react-aria-components';
  */
 export function OpenAPIDisclosure(props: {
     icon: React.ReactNode;
+    header: React.ReactNode;
     children: React.ReactNode;
     label: string | ((isExpanded: boolean) => string);
     className?: string;
 }): React.JSX.Element {
-    const { icon, children, label, className } = props;
+    const { icon, header, label, children, className } = props;
     const [isExpanded, setIsExpanded] = useState(false);
 
     return (
@@ -31,8 +32,11 @@ export function OpenAPIDisclosure(props: {
                         : 'none',
                 })}
             >
-                {icon}
-                <span>{typeof label === 'function' ? label(isExpanded) : label}</span>
+                {header}
+                <div className="openapi-disclosure-trigger-label">
+                    <span>{typeof label === 'function' ? label(isExpanded) : label}</span>
+                    {icon}
+                </div>
             </Button>
             <DisclosurePanel className="openapi-disclosure-panel">
                 {isExpanded ? children : null}
diff --git a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
index 7f2a428d5c..ecef441b76 100644
--- a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
+++ b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
@@ -76,7 +76,7 @@ function DisclosureItem(props: {
     });
 
     const panelRef = useRef<HTMLDivElement | null>(null);
-    const triggerRef = useRef<HTMLButtonElement | null>(null);
+    const triggerRef = useRef<HTMLDivElement | null>(null);
     const isDisabled = groupState?.isDisabled || !group.tabs?.length || false;
     const { buttonProps: triggerProps, panelProps } = useDisclosure(
         {
@@ -96,55 +96,56 @@ function DisclosureItem(props: {
 
     return (
         <div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
-            <div className="openapi-disclosure-group-header">
-                <button
-                    slot="trigger"
-                    ref={triggerRef}
-                    {...mergeProps(buttonProps, focusProps)}
-                    disabled={isDisabled}
-                    style={{
-                        outline: isFocusVisible
-                            ? '2px solid rgb(var(--primary-color-500)/0.4)'
-                            : 'none',
-                    }}
-                    className="openapi-disclosure-group-trigger"
-                >
-                    <div className="openapi-disclosure-group-icon">
-                        {icon || (
-                            <svg viewBox="0 0 24 24" className="openapi-disclosure-group-icon">
-                                <path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
-                            </svg>
-                        )}
-                    </div>
+            <div
+                slot="trigger"
+                ref={triggerRef}
+                {...mergeProps(buttonProps, focusProps)}
+                aria-disabled={isDisabled}
+                style={{
+                    outline: isFocusVisible
+                        ? '2px solid rgb(var(--primary-color-500)/0.4)'
+                        : 'none',
+                }}
+                className="openapi-disclosure-group-trigger"
+            >
+                <div className="openapi-disclosure-group-icon">
+                    {icon || (
+                        <svg viewBox="0 0 24 24" className="openapi-disclosure-group-icon">
+                            <path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
+                        </svg>
+                    )}
+                </div>
 
+                <div className="openapi-disclosure-group-label">
                     {group.label}
-                </button>
-                {group.tabs ? (
-                    <div
-                        className="openapi-disclosure-group-mediatype"
-                        onClick={(e) => e.stopPropagation()}
-                    >
-                        {group.tabs?.length > 1 ? (
-                            <OpenAPISelect
-                                icon={selectIcon}
-                                stateKey={selectStateKey}
-                                onSelectionChange={() => {
-                                    state.expand();
-                                }}
-                                items={group.tabs}
-                                placement="bottom end"
-                            >
-                                {group.tabs.map((tab) => (
-                                    <OpenAPISelectItem key={tab.key} id={tab.key} value={tab}>
-                                        {tab.label}
-                                    </OpenAPISelectItem>
-                                ))}
-                            </OpenAPISelect>
-                        ) : group.tabs[0]?.label ? (
-                            <span>{group.tabs[0].label}</span>
-                        ) : null}
-                    </div>
-                ) : null}
+
+                    {group.tabs ? (
+                        <div
+                            className="openapi-disclosure-group-mediatype"
+                            onClick={(e) => e.stopPropagation()}
+                        >
+                            {group.tabs?.length > 1 ? (
+                                <OpenAPISelect
+                                    icon={selectIcon}
+                                    stateKey={selectStateKey}
+                                    onSelectionChange={() => {
+                                        state.expand();
+                                    }}
+                                    items={group.tabs}
+                                    placement="bottom end"
+                                >
+                                    {group.tabs.map((tab) => (
+                                        <OpenAPISelectItem key={tab.key} id={tab.key} value={tab}>
+                                            {tab.label}
+                                        </OpenAPISelectItem>
+                                    ))}
+                                </OpenAPISelect>
+                            ) : group.tabs[0]?.label ? (
+                                <span>{group.tabs[0].label}</span>
+                            ) : null}
+                        </div>
+                    ) : null}
+                </div>
             </div>
 
             {state.isExpanded && selectedTab && (
diff --git a/packages/react-openapi/src/OpenAPIResponse.tsx b/packages/react-openapi/src/OpenAPIResponse.tsx
index de744b7589..3c005ec29b 100644
--- a/packages/react-openapi/src/OpenAPIResponse.tsx
+++ b/packages/react-openapi/src/OpenAPIResponse.tsx
@@ -1,7 +1,9 @@
 import type { OpenAPIV3 } from '@gitbook/openapi-parser';
 import { OpenAPIDisclosure } from './OpenAPIDisclosure';
+import { OpenAPISchemaPresentation } from './OpenAPISchema';
 import { OpenAPISchemaProperties } from './OpenAPISchemaServer';
 import type { OpenAPIClientContext } from './context';
+import { tString } from './translate';
 import { parameterToProperty, resolveDescription } from './utils';
 
 /**
@@ -27,7 +29,31 @@ export function OpenAPIResponse(props: {
     return (
         <div className="openapi-response-body">
             {headers.length > 0 ? (
-                <OpenAPIDisclosure icon={context.icons.plus} label="Headers">
+                <OpenAPIDisclosure
+                    header={
+                        <OpenAPISchemaPresentation
+                            context={context}
+                            property={{
+                                propertyName: tString(context.translation, 'headers'),
+                                schema: {
+                                    type: 'object',
+                                },
+                                required: null,
+                            }}
+                        />
+                    }
+                    icon={context.icons.plus}
+                    label={(isExpanded) =>
+                        tString(
+                            context.translation,
+                            isExpanded ? 'hide' : 'show',
+                            tString(
+                                context.translation,
+                                headers.length === 1 ? 'header' : 'headers'
+                            )
+                        )
+                    }
+                >
                     <OpenAPISchemaProperties
                         properties={headers.map(([name, header]) =>
                             parameterToProperty({ name, ...header })
@@ -40,7 +66,13 @@ export function OpenAPIResponse(props: {
                 <div className="openapi-responsebody">
                     <OpenAPISchemaProperties
                         id={`response-${context.blockKey}`}
-                        properties={[{ schema: mediaType.schema }]}
+                        properties={[
+                            {
+                                schema: mediaType.schema,
+                                propertyName: tString(context.translation, 'response'),
+                                required: null,
+                            },
+                        ]}
                         context={context}
                     />
                 </div>
diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx
index 6d1cfdac25..b4b865f505 100644
--- a/packages/react-openapi/src/OpenAPIResponseExample.tsx
+++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx
@@ -6,8 +6,8 @@ import { OpenAPIResponseExampleContent } from './OpenAPIResponseExampleContent';
 import { type OpenAPIContext, getOpenAPIClientContext } from './context';
 import type { OpenAPIOperationData, OpenAPIWebhookData } from './types';
 import { getExampleFromReference, getExamples } from './util/example';
-import { createStateKey, getStatusCodeDefaultLabel } from './utils';
-import { checkIsReference, resolveDescription } from './utils';
+import { createStateKey, getStatusCodeDefaultLabel, resolveDescription } from './utils';
+import { checkIsReference } from './utils';
 
 /**
  * Display an example of the response content.
@@ -41,45 +41,47 @@ export function OpenAPIResponseExample(props: {
         return Number(a) - Number(b);
     });
 
-    const tabs = responses.map(([key, responseObject]) => {
-        const description = resolveDescription(responseObject);
-        const label = description ? (
-            <Markdown source={description} />
-        ) : (
-            getStatusCodeDefaultLabel(key, context)
-        );
+    const tabs = responses
+        .filter(([_, responseObject]) => responseObject && typeof responseObject === 'object')
+        .map(([key, responseObject]) => {
+            const description = resolveDescription(responseObject);
+            const label = description ? (
+                <Markdown source={description} />
+            ) : (
+                getStatusCodeDefaultLabel(key, context)
+            );
 
-        if (checkIsReference(responseObject)) {
-            return {
-                key: key,
-                label,
-                statusCode: key,
-                body: (
-                    <OpenAPIExample
-                        example={getExampleFromReference(responseObject, context)}
-                        context={context}
-                        syntax="json"
-                    />
-                ),
-            };
-        }
+            if (checkIsReference(responseObject)) {
+                return {
+                    key: key,
+                    label,
+                    statusCode: key,
+                    body: (
+                        <OpenAPIExample
+                            example={getExampleFromReference(responseObject, context)}
+                            context={context}
+                            syntax="json"
+                        />
+                    ),
+                };
+            }
+
+            if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
+                return {
+                    key: key,
+                    label,
+                    statusCode: key,
+                    body: <OpenAPIEmptyExample context={context} />,
+                };
+            }
 
-        if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
             return {
                 key: key,
                 label,
                 statusCode: key,
-                body: <OpenAPIEmptyExample context={context} />,
+                body: <OpenAPIResponse context={context} content={responseObject.content} />,
             };
-        }
-
-        return {
-            key: key,
-            label,
-            statusCode: key,
-            body: <OpenAPIResponse context={context} content={responseObject.content} />,
-        };
-    });
+        });
 
     if (tabs.length === 0) {
         return null;
diff --git a/packages/react-openapi/src/OpenAPIResponses.tsx b/packages/react-openapi/src/OpenAPIResponses.tsx
index efee2b3ce8..c4565aa788 100644
--- a/packages/react-openapi/src/OpenAPIResponses.tsx
+++ b/packages/react-openapi/src/OpenAPIResponses.tsx
@@ -20,8 +20,9 @@ export function OpenAPIResponses(props: {
 }) {
     const { responses, context } = props;
 
-    const groups = Object.entries(responses).map(
-        ([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
+    const groups = Object.entries(responses)
+        .filter(([_, response]) => response && typeof response === 'object')
+        .map(([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
             const tabs = (() => {
                 // If there is no content, but there are headers, we need to show the headers
                 if (
@@ -83,8 +84,7 @@ export function OpenAPIResponses(props: {
                 ),
                 tabs,
             };
-        }
-    );
+        });
 
     const state = useResponseExamplesState(context.blockKey, groups[0]?.key);
 
diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx
index d268b17cf5..e9f8c6707b 100644
--- a/packages/react-openapi/src/OpenAPISchema.tsx
+++ b/packages/react-openapi/src/OpenAPISchema.tsx
@@ -4,6 +4,7 @@
 
 import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser';
 import { useId } from 'react';
+import type { ComponentPropsWithoutRef } from 'react';
 
 import clsx from 'clsx';
 import { Markdown } from './Markdown';
@@ -12,6 +13,7 @@ import { OpenAPIDisclosure } from './OpenAPIDisclosure';
 import { OpenAPISchemaName } from './OpenAPISchemaName';
 import type { OpenAPIClientContext } from './context';
 import { retrocycle } from './decycle';
+import { getDisclosureLabel } from './getDisclosureLabel';
 import { stringifyOpenAPI } from './stringifyOpenAPI';
 import { tString } from './translate';
 import { checkIsReference, resolveDescription, resolveFirstExample } from './utils';
@@ -19,73 +21,97 @@ import { checkIsReference, resolveDescription, resolveFirstExample } from './uti
 type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
 
 export interface OpenAPISchemaPropertyEntry {
-    propertyName?: string | undefined;
-    required?: boolean | undefined;
+    propertyName?: string;
+    required?: boolean | null;
     schema: OpenAPIV3.SchemaObject;
 }
 
 /**
  * Render a property of an OpenAPI schema.
  */
-function OpenAPISchemaProperty(props: {
-    property: OpenAPISchemaPropertyEntry;
-    context: OpenAPIClientContext;
-    circularRefs: CircularRefsIds;
-    className?: string;
-}) {
-    const { circularRefs: parentCircularRefs, context, className, property } = props;
+function OpenAPISchemaProperty(
+    props: {
+        property: OpenAPISchemaPropertyEntry;
+        context: OpenAPIClientContext;
+        circularRefs: CircularRefsIds;
+        className?: string;
+    } & Omit<ComponentPropsWithoutRef<'div'>, 'property' | 'context' | 'circularRefs' | 'className'>
+) {
+    const { circularRefs: parentCircularRefs, context, className, property, ...rest } = props;
 
     const { schema } = property;
 
     const id = useId();
 
-    return (
-        <div id={id} className={clsx('openapi-schema', className)}>
-            <OpenAPISchemaPresentation context={context} property={property} />
-            {(() => {
-                const circularRefId = parentCircularRefs.get(schema);
-                // Avoid recursing infinitely, and instead render a link to the parent schema
-                if (circularRefId) {
-                    return <OpenAPISchemaCircularRef id={circularRefId} schema={schema} />;
-                }
+    const circularRefId = parentCircularRefs.get(schema);
+    // Avoid recursing infinitely, and instead render a link to the parent schema
+    if (circularRefId) {
+        return <OpenAPISchemaCircularRef id={circularRefId} schema={schema} />;
+    }
+
+    const circularRefs = new Map(parentCircularRefs);
+    circularRefs.set(schema, id);
+
+    const properties = getSchemaProperties(schema);
+
+    const ancestors = new Set(circularRefs.keys());
+    const alternatives = getSchemaAlternatives(schema, ancestors);
+
+    const header = <OpenAPISchemaPresentation context={context} property={property} />;
+    const content = (() => {
+        if (properties?.length) {
+            return (
+                <OpenAPISchemaProperties
+                    properties={properties}
+                    circularRefs={circularRefs}
+                    context={context}
+                />
+            );
+        }
 
-                const circularRefs = new Map(parentCircularRefs);
-                circularRefs.set(schema, id);
-
-                const properties = getSchemaProperties(schema);
-                if (properties?.length) {
-                    return (
-                        <OpenAPIDisclosure
-                            icon={context.icons.plus}
-                            label={(isExpanded) =>
-                                getDisclosureLabel({ schema, isExpanded, context })
-                            }
-                        >
-                            <OpenAPISchemaProperties
-                                properties={properties}
+        if (alternatives) {
+            return (
+                <div className="openapi-schema-alternatives">
+                    {alternatives.map((alternativeSchema, index) => (
+                        <div key={index} className="openapi-schema-alternative">
+                            <OpenAPISchemaAlternative
+                                schema={alternativeSchema}
                                 circularRefs={circularRefs}
                                 context={context}
                             />
-                        </OpenAPIDisclosure>
-                    );
-                }
+                            {index < alternatives.length - 1 ? (
+                                <OpenAPISchemaAlternativeSeparator
+                                    schema={schema}
+                                    context={context}
+                                />
+                            ) : null}
+                        </div>
+                    ))}
+                </div>
+            );
+        }
 
-                const ancestors = new Set(circularRefs.keys());
-                const alternatives = getSchemaAlternatives(schema, ancestors);
-
-                if (alternatives) {
-                    return alternatives.map((schema, index) => (
-                        <OpenAPISchemaAlternative
-                            key={index}
-                            schema={schema}
-                            circularRefs={circularRefs}
-                            context={context}
-                        />
-                    ));
-                }
+        return null;
+    })();
 
-                return null;
-            })()}
+    if (properties?.length) {
+        return (
+            <OpenAPIDisclosure
+                icon={context.icons.plus}
+                className={clsx('openapi-schema', className)}
+                header={header}
+                label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })}
+                {...rest}
+            >
+                {content}
+            </OpenAPIDisclosure>
+        );
+    }
+
+    return (
+        <div id={id} {...rest} className={clsx('openapi-schema', className)}>
+            {header}
+            {content}
         </div>
     );
 }
@@ -115,6 +141,7 @@ function OpenAPISchemaProperties(props: {
                         circularRefs={circularRefs}
                         property={property}
                         context={context}
+                        style={{ animationDelay: `${index * 0.02}s` }}
                     />
                 );
             })}
@@ -205,34 +232,48 @@ function OpenAPISchemaAlternative(props: {
     context: OpenAPIClientContext;
 }) {
     const { schema, circularRefs, context } = props;
-
-    const description = resolveDescription(schema);
     const properties = getSchemaProperties(schema);
 
+    return properties?.length ? (
+        <OpenAPIDisclosure
+            icon={context.icons.plus}
+            header={<OpenAPISchemaPresentation property={{ schema }} context={context} />}
+            label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })}
+        >
+            <OpenAPISchemaProperties
+                properties={properties}
+                circularRefs={circularRefs}
+                context={context}
+            />
+        </OpenAPIDisclosure>
+    ) : (
+        <OpenAPISchemaProperty
+            property={{ schema }}
+            circularRefs={circularRefs}
+            context={context}
+        />
+    );
+}
+
+function OpenAPISchemaAlternativeSeparator(props: {
+    schema: OpenAPIV3.SchemaObject;
+    context: OpenAPIClientContext;
+}) {
+    const { schema, context } = props;
+
+    const anyOf = schema.anyOf || schema.items?.anyOf;
+    const oneOf = schema.oneOf || schema.items?.oneOf;
+    const allOf = schema.allOf || schema.items?.allOf;
+
+    if (!anyOf && !oneOf && !allOf) {
+        return null;
+    }
+
     return (
-        <>
-            {description ? (
-                <Markdown source={description} className="openapi-schema-description" />
-            ) : null}
-            <OpenAPIDisclosure
-                icon={context.icons.plus}
-                label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })}
-            >
-                {properties?.length ? (
-                    <OpenAPISchemaProperties
-                        properties={properties}
-                        circularRefs={circularRefs}
-                        context={context}
-                    />
-                ) : (
-                    <OpenAPISchemaProperty
-                        property={{ schema }}
-                        circularRefs={circularRefs}
-                        context={context}
-                    />
-                )}
-            </OpenAPIDisclosure>
-        </>
+        <span className="openapi-schema-alternative-separator">
+            {(anyOf || oneOf) && tString(context.translation, 'or')}
+            {allOf && tString(context.translation, 'and')}
+        </span>
     );
 }
 
@@ -293,7 +334,7 @@ function OpenAPISchemaEnum(props: {
 
     return (
         <span className="openapi-schema-enum">
-            Available options:{' '}
+            {tString(context.translation, 'possible_values')}:{' '}
             {enumValues.map((item, index) => (
                 <span key={index} className="openapi-schema-enum-value">
                     <OpenAPICopyButton
@@ -313,7 +354,7 @@ function OpenAPISchemaEnum(props: {
 /**
  * Render the top row of a schema. e.g: name, type, and required status.
  */
-function OpenAPISchemaPresentation(props: {
+export function OpenAPISchemaPresentation(props: {
     property: OpenAPISchemaPropertyEntry;
     context: OpenAPIClientContext;
 }) {
@@ -437,6 +478,14 @@ export function getSchemaAlternatives(
     schema: OpenAPIV3.SchemaObject,
     ancestors: Set<OpenAPIV3.SchemaObject> = new Set()
 ): OpenAPIV3.SchemaObject[] | null {
+    // Search for alternatives in the items property if it exists
+    if (
+        schema.items &&
+        ('oneOf' in schema.items || 'allOf' in schema.items || 'anyOf' in schema.items)
+    ) {
+        return getSchemaAlternatives(schema.items, ancestors);
+    }
+
     const alternatives:
         | [AlternativeType, (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[]]
         | null = (() => {
@@ -552,6 +601,9 @@ function flattenAlternatives(
     schemasOrRefs: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[],
     ancestors: Set<OpenAPIV3.SchemaObject>
 ): OpenAPIV3.SchemaObject[] {
+    // Get the parent schema's required fields from the most recent ancestor
+    const latestAncestor = Array.from(ancestors).pop();
+
     return schemasOrRefs.reduce<OpenAPIV3.SchemaObject[]>((acc, schemaOrRef) => {
         if (checkIsReference(schemaOrRef)) {
             return acc;
@@ -560,16 +612,47 @@ function flattenAlternatives(
         if (schemaOrRef[alternativeType] && !ancestors.has(schemaOrRef)) {
             const schemas = getSchemaAlternatives(schemaOrRef, ancestors);
             if (schemas) {
-                acc.push(...schemas);
+                acc.push(
+                    ...schemas.map((schema) => ({
+                        ...schema,
+                        required: mergeRequiredFields(schema, latestAncestor),
+                    }))
+                );
             }
             return acc;
         }
 
-        acc.push(schemaOrRef);
+        // For direct schemas, handle required fields
+        const schema = {
+            ...schemaOrRef,
+            required: mergeRequiredFields(schemaOrRef, latestAncestor),
+        };
+
+        acc.push(schema);
         return acc;
     }, []);
 }
 
+/**
+ * Merge the required fields of a schema with the required fields of its latest ancestor.
+ */
+function mergeRequiredFields(
+    schemaOrRef: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
+    latestAncestor: OpenAPIV3.SchemaObject | undefined
+) {
+    if (!schemaOrRef.required && !latestAncestor?.required) {
+        return undefined;
+    }
+
+    if (checkIsReference(schemaOrRef)) {
+        return latestAncestor?.required;
+    }
+
+    return Array.from(
+        new Set([...(latestAncestor?.required || []), ...(schemaOrRef.required || [])])
+    );
+}
+
 function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
     // Otherwise try to infer a nice title
     let type = 'any';
@@ -587,6 +670,11 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
         if (schema.format) {
             type += ` · ${schema.format}`;
         }
+
+        // Only add the title if it's an object (no need for the title of a string, number, etc.)
+        if (type === 'object' && schema.title) {
+            type += ` · ${schema.title.replaceAll(' ', '')}`;
+        }
     }
 
     if ('anyOf' in schema) {
@@ -601,27 +689,3 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
 
     return type;
 }
-
-function getDisclosureLabel(props: {
-    schema: OpenAPIV3.SchemaObject;
-    isExpanded: boolean;
-    context: OpenAPIClientContext;
-}) {
-    const { schema, isExpanded, context } = props;
-    let label: string;
-    if (schema.type === 'array' && !!schema.items) {
-        if (schema.items.oneOf) {
-            label = tString(context.translation, 'available_items').toLowerCase();
-        }
-        // Fallback to "child attributes" for enums and objects
-        else if (schema.items.enum || schema.items.type === 'object') {
-            label = tString(context.translation, 'child_attributes').toLowerCase();
-        } else {
-            label = schema.items.title ?? schema.title ?? getSchemaTitle(schema.items);
-        }
-    } else {
-        label = schema.title || tString(context.translation, 'child_attributes').toLowerCase();
-    }
-
-    return `${isExpanded ? tString(context.translation, 'hide') : tString(context.translation, 'show')} ${label}`;
-}
diff --git a/packages/react-openapi/src/OpenAPISchemaName.tsx b/packages/react-openapi/src/OpenAPISchemaName.tsx
index 6e230419cd..8994b4d71f 100644
--- a/packages/react-openapi/src/OpenAPISchemaName.tsx
+++ b/packages/react-openapi/src/OpenAPISchemaName.tsx
@@ -6,7 +6,7 @@ import { t, tString } from './translate';
 interface OpenAPISchemaNameProps {
     schema?: OpenAPIV3.SchemaObject;
     propertyName?: string | React.JSX.Element;
-    required?: boolean;
+    required?: boolean | null;
     type?: string;
     context: OpenAPIClientContext;
 }
@@ -27,12 +27,14 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
                     {propertyName}
                 </span>
             ) : null}
-            <span>
-                {type ? <span className="openapi-schema-type">{type}</span> : null}
-                {additionalItems ? (
-                    <span className="openapi-schema-type">{additionalItems}</span>
-                ) : null}
-            </span>
+            {type || additionalItems ? (
+                <span>
+                    {type ? <span className="openapi-schema-type">{type}</span> : null}
+                    {additionalItems ? (
+                        <span className="openapi-schema-type">{additionalItems}</span>
+                    ) : null}
+                </span>
+            ) : null}
             {schema?.readOnly ? (
                 <span className="openapi-schema-readonly">
                     {t(context.translation, 'read_only')}
@@ -43,7 +45,7 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) {
                     {t(context.translation, 'write_only')}
                 </span>
             ) : null}
-            {required ? (
+            {required === null ? null : required ? (
                 <span className="openapi-schema-required">
                     {t(context.translation, 'required')}
                 </span>
diff --git a/packages/react-openapi/src/OpenAPISecurities.tsx b/packages/react-openapi/src/OpenAPISecurities.tsx
index 3f86236506..8b007ff3c1 100644
--- a/packages/react-openapi/src/OpenAPISecurities.tsx
+++ b/packages/react-openapi/src/OpenAPISecurities.tsx
@@ -1,5 +1,7 @@
+import type { OpenAPIV3 } from '@gitbook/openapi-parser';
 import { InteractiveSection } from './InteractiveSection';
 import { Markdown } from './Markdown';
+import { OpenAPICopyButton } from './OpenAPICopyButton';
 import { OpenAPISchemaName } from './OpenAPISchemaName';
 import type { OpenAPIClientContext } from './context';
 import { t } from './translate';
@@ -105,13 +107,7 @@ function getLabelForType(security: OpenAPISecurityWithRequired, context: OpenAPI
                 />
             );
         case 'oauth2':
-            return (
-                <OpenAPISchemaName
-                    context={context}
-                    propertyName="OAuth2"
-                    required={security.required}
-                />
-            );
+            return <OpenAPISchemaOAuth2Flows context={context} security={security} />;
         case 'openIdConnect':
             return (
                 <OpenAPISchemaName
@@ -125,3 +121,111 @@ function getLabelForType(security: OpenAPISecurityWithRequired, context: OpenAPI
             return security.type;
     }
 }
+
+function OpenAPISchemaOAuth2Flows(props: {
+    context: OpenAPIClientContext;
+    security: OpenAPIV3.OAuth2SecurityScheme & { required?: boolean };
+}) {
+    const { context, security } = props;
+
+    const flows = Object.entries(security.flows ?? {});
+
+    return (
+        <div className="openapi-securities-oauth-flows">
+            {flows.map(([name, flow], index) => (
+                <OpenAPISchemaOAuth2Item
+                    key={index}
+                    flow={flow}
+                    name={name}
+                    context={context}
+                    security={security}
+                />
+            ))}
+        </div>
+    );
+}
+
+function OpenAPISchemaOAuth2Item(props: {
+    flow: NonNullable<OpenAPIV3.OAuth2SecurityScheme['flows']>[keyof NonNullable<
+        OpenAPIV3.OAuth2SecurityScheme['flows']
+    >];
+    name: string;
+    context: OpenAPIClientContext;
+    security: OpenAPIV3.OAuth2SecurityScheme & { required?: boolean };
+}) {
+    const { flow, context, security, name } = props;
+
+    if (!flow) {
+        return null;
+    }
+
+    const scopes = Object.entries(flow?.scopes ?? {});
+
+    return (
+        <div>
+            <OpenAPISchemaName
+                context={context}
+                propertyName="OAuth2"
+                type={name}
+                required={security.required}
+            />
+            <div className="openapi-securities-oauth-content openapi-markdown">
+                {security.description ? <Markdown source={security.description} /> : null}
+                {'authorizationUrl' in flow && flow.authorizationUrl ? (
+                    <span>
+                        Authorization URL:{' '}
+                        <OpenAPICopyButton
+                            value={flow.authorizationUrl}
+                            context={context}
+                            className="openapi-securities-url"
+                            withTooltip
+                        >
+                            {flow.authorizationUrl}
+                        </OpenAPICopyButton>
+                    </span>
+                ) : null}
+                {'tokenUrl' in flow && flow.tokenUrl ? (
+                    <span>
+                        Token URL:{' '}
+                        <OpenAPICopyButton
+                            value={flow.tokenUrl}
+                            context={context}
+                            className="openapi-securities-url"
+                            withTooltip
+                        >
+                            {flow.tokenUrl}
+                        </OpenAPICopyButton>
+                    </span>
+                ) : null}
+                {'refreshUrl' in flow && flow.refreshUrl ? (
+                    <span>
+                        Refresh URL:{' '}
+                        <OpenAPICopyButton
+                            value={flow.refreshUrl}
+                            context={context}
+                            className="openapi-securities-url"
+                            withTooltip
+                        >
+                            {flow.refreshUrl}
+                        </OpenAPICopyButton>
+                    </span>
+                ) : null}
+                {scopes.length ? (
+                    <div>
+                        {t(context.translation, 'available_scopes')}:{' '}
+                        <ul>
+                            {scopes.map(([key, value]) => (
+                                <li key={key}>
+                                    <OpenAPICopyButton value={key} context={context} withTooltip>
+                                        <code>{key}</code>
+                                    </OpenAPICopyButton>
+                                    : {value}
+                                </li>
+                            ))}
+                        </ul>
+                    </div>
+                ) : null}
+            </div>
+        </div>
+    );
+}
diff --git a/packages/react-openapi/src/OpenAPISpec.tsx b/packages/react-openapi/src/OpenAPISpec.tsx
index 49c41cda40..1e6e562690 100644
--- a/packages/react-openapi/src/OpenAPISpec.tsx
+++ b/packages/react-openapi/src/OpenAPISpec.tsx
@@ -18,7 +18,7 @@ export function OpenAPISpec(props: {
 
     const { operation } = data;
 
-    const parameters = operation.parameters ?? [];
+    const parameters = deduplicateParameters(operation.parameters ?? []);
     const parameterGroups = groupParameters(parameters, context);
 
     const securities = 'securities' in data ? data.securities : [];
@@ -113,3 +113,23 @@ function getParameterGroupName(paramIn: string, context: OpenAPIClientContext):
             return paramIn;
     }
 }
+
+/** Deduplicate parameters by name and in.
+ * Some specs have both parameters define at path and operation level.
+ * We only want to display one of them.
+ */
+function deduplicateParameters(parameters: OpenAPI.Parameters): OpenAPI.Parameters {
+    const seen = new Set();
+
+    return parameters.filter((param) => {
+        const key = `${param.name}:${param.in}`;
+
+        if (seen.has(key)) {
+            return false;
+        }
+
+        seen.add(key);
+
+        return true;
+    });
+}
diff --git a/packages/react-openapi/src/StaticSection.tsx b/packages/react-openapi/src/StaticSection.tsx
index ffb3a2bba8..b79b20e617 100644
--- a/packages/react-openapi/src/StaticSection.tsx
+++ b/packages/react-openapi/src/StaticSection.tsx
@@ -11,7 +11,7 @@ export function SectionHeader(props: ComponentPropsWithoutRef<'div'>) {
             {...props}
             className={clsx(
                 'openapi-section-header',
-                props.className && `${props.className}-header`
+                props.className ? `${props.className}-header` : undefined
             )}
         />
     );
diff --git a/packages/react-openapi/src/code-samples.test.ts b/packages/react-openapi/src/code-samples.test.ts
index 363baf5525..375cee84be 100644
--- a/packages/react-openapi/src/code-samples.test.ts
+++ b/packages/react-openapi/src/code-samples.test.ts
@@ -400,7 +400,7 @@ describe('python code sample generator', () => {
         const output = generator?.generate(input);
 
         expect(output).toBe(
-            'import requests\n\nresponse = requests.get(\n    "https://example.com/path",\n    headers={"Content-Type":"application/x-www-form-urlencoded"},\n    data={"key":"value"}\n)\n\ndata = response.json()'
+            'import requests\n\nresponse = requests.get(\n    "https://example.com/path",\n    headers={"Content-Type":"application/x-www-form-urlencoded"},\n    data={\n      "key": "value"\n    }\n)\n\ndata = response.json()'
         );
     });
 
@@ -415,13 +415,14 @@ describe('python code sample generator', () => {
                 key: 'value',
                 truethy: true,
                 falsey: false,
+                nullish: null,
             },
         };
 
         const output = generator?.generate(input);
 
         expect(output).toBe(
-            'import requests\n\nresponse = requests.get(\n    "https://example.com/path",\n    headers={"Content-Type":"application/json"},\n    data=json.dumps({"key":"value","truethy":True,"falsey":False})\n)\n\ndata = response.json()'
+            'import requests\n\nresponse = requests.get(\n    "https://example.com/path",\n    headers={"Content-Type":"application/json"},\n    data=json.dumps({\n      "key": "value",\n      "truethy": True,\n      "falsey": False,\n      "nullish": None\n    })\n)\n\ndata = response.json()'
         );
     });
 
diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts
index fe44da11d8..8d855bbb67 100644
--- a/packages/react-openapi/src/code-samples.ts
+++ b/packages/react-openapi/src/code-samples.ts
@@ -30,7 +30,7 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
     {
         id: 'http',
         label: 'HTTP',
-        syntax: 'bash',
+        syntax: 'http',
         generate: ({ method, url, headers = {}, body }: CodeSampleInput) => {
             const { host, path } = parseHostAndPath(url);
 
@@ -356,18 +356,25 @@ const BodyGenerators = {
             // Convert JSON to XML if needed
             body = JSON.stringify(convertBodyToXML(body));
         } else {
-            body = stringifyOpenAPI(body, (_key, value) => {
-                switch (value) {
-                    case true:
-                        return '$$__TRUE__$$';
-                    case false:
-                        return '$$__FALSE__$$';
-                    default:
-                        return value;
-                }
-            })
+            body = stringifyOpenAPI(
+                body,
+                (_key, value) => {
+                    switch (value) {
+                        case true:
+                            return '$$__TRUE__$$';
+                        case false:
+                            return '$$__FALSE__$$';
+                        case null:
+                            return '$$__NULL__$$';
+                        default:
+                            return value;
+                    }
+                },
+                2
+            )
                 .replaceAll('"$$__TRUE__$$"', 'True')
-                .replaceAll('"$$__FALSE__$$"', 'False');
+                .replaceAll('"$$__FALSE__$$"', 'False')
+                .replaceAll('"$$__NULL__$$"', 'None');
         }
 
         return { body, code, headers };
diff --git a/packages/react-openapi/src/generateSchemaExample.test.ts b/packages/react-openapi/src/generateSchemaExample.test.ts
index 3181881b62..682c5f2f38 100644
--- a/packages/react-openapi/src/generateSchemaExample.test.ts
+++ b/packages/react-openapi/src/generateSchemaExample.test.ts
@@ -1017,4 +1017,24 @@ describe('generateSchemaExample', () => {
             },
         });
     });
+
+    it('handles deprecated properties', () => {
+        expect(
+            generateSchemaExample({
+                type: 'object',
+                deprecated: true,
+            })
+        ).toBeUndefined();
+    });
+
+    it('handle nested deprecated properties', () => {
+        expect(
+            generateSchemaExample({
+                type: 'array',
+                items: {
+                    deprecated: true,
+                },
+            })
+        ).toBeUndefined();
+    });
 });
diff --git a/packages/react-openapi/src/generateSchemaExample.ts b/packages/react-openapi/src/generateSchemaExample.ts
index 37695b9fe6..595f723d91 100644
--- a/packages/react-openapi/src/generateSchemaExample.ts
+++ b/packages/react-openapi/src/generateSchemaExample.ts
@@ -167,7 +167,7 @@ const getExampleFromSchema = (
     const makeUpRandomData = !!options?.emptyString;
 
     // If the property is deprecated we don't show it in examples.
-    if (schema.deprecated) {
+    if (schema.deprecated || (schema.type === 'array' && schema.items?.deprecated)) {
         return undefined;
     }
 
@@ -204,6 +204,14 @@ const getExampleFromSchema = (
         return cache(schema, schema.example);
     }
 
+    // Use a default value, if there’s one and it’s a string or number
+    if (
+        schema.default !== undefined &&
+        ['string', 'number', 'boolean'].includes(typeof schema.default)
+    ) {
+        return cache(schema, schema.default);
+    }
+
     // enum: [ 'available', 'pending', 'sold' ]
     if (Array.isArray(schema.enum) && schema.enum.length > 0) {
         return cache(schema, schema.enum[0]);
diff --git a/packages/react-openapi/src/getDisclosureLabel.ts b/packages/react-openapi/src/getDisclosureLabel.ts
new file mode 100644
index 0000000000..814113b910
--- /dev/null
+++ b/packages/react-openapi/src/getDisclosureLabel.ts
@@ -0,0 +1,25 @@
+'use client';
+
+import type { OpenAPIV3 } from '@gitbook/openapi-parser';
+import type { OpenAPIClientContext } from './context';
+import { tString } from './translate';
+
+export function getDisclosureLabel(props: {
+    schema: OpenAPIV3.SchemaObject;
+    isExpanded: boolean;
+    context: OpenAPIClientContext;
+}) {
+    const { schema, isExpanded, context } = props;
+    let label: string;
+    if (schema.type === 'array' && !!schema.items) {
+        if (schema.items.oneOf) {
+            label = tString(context.translation, 'available_items').toLowerCase();
+        } else {
+            label = tString(context.translation, 'properties').toLowerCase();
+        }
+    } else {
+        label = tString(context.translation, 'properties').toLowerCase();
+    }
+
+    return tString(context.translation, isExpanded ? 'hide' : 'show', label);
+}
diff --git a/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx b/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx
new file mode 100644
index 0000000000..86b7d49e8f
--- /dev/null
+++ b/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+import { SectionBody } from '../StaticSection';
+
+import type { OpenAPIV3 } from '@gitbook/openapi-parser';
+import { OpenAPIDisclosure } from '../OpenAPIDisclosure';
+import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
+import { Section } from '../StaticSection';
+import type { OpenAPIClientContext } from '../context';
+import { getDisclosureLabel } from '../getDisclosureLabel';
+
+export function OpenAPISchemaItem(props: {
+    name: string;
+    schema: OpenAPIV3.SchemaObject;
+    context: OpenAPIClientContext;
+}) {
+    const { schema, context, name } = props;
+
+    return (
+        <OpenAPIDisclosure
+            className="openapi-schemas-disclosure"
+            key={name}
+            icon={context.icons.plus}
+            header={name}
+            label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })}
+        >
+            <Section className="openapi-section-schemas">
+                <SectionBody>
+                    <OpenAPIRootSchema schema={schema} context={context} />
+                </SectionBody>
+            </Section>
+        </OpenAPIDisclosure>
+    );
+}
diff --git a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx
index 2283245fcb..c0700a530a 100644
--- a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx
+++ b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx
@@ -1,9 +1,8 @@
 import type { OpenAPISchema } from '@gitbook/openapi-parser';
 import clsx from 'clsx';
-import { OpenAPIDisclosure } from '../OpenAPIDisclosure';
 import { OpenAPIExample } from '../OpenAPIExample';
 import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
-import { Section, SectionBody, StaticSection } from '../StaticSection';
+import { StaticSection } from '../StaticSection';
 import {
     type OpenAPIContextInput,
     getOpenAPIClientContext,
@@ -11,6 +10,7 @@ import {
 } from '../context';
 import { t } from '../translate';
 import { getExampleFromSchema } from '../util/example';
+import { OpenAPISchemaItem } from './OpenAPISchemaItem';
 
 /**
  * OpenAPI Schemas component.
@@ -85,18 +85,12 @@ export function OpenAPISchemas(props: {
         <div className={clsx('openapi-schemas', className)}>
             {schemas.map(({ name, schema }) => {
                 return (
-                    <OpenAPIDisclosure
-                        className="openapi-schemas-disclosure"
+                    <OpenAPISchemaItem
                         key={name}
-                        icon={context.icons.chevronRight}
-                        label={name}
-                    >
-                        <Section className="openapi-section-schemas">
-                            <SectionBody>
-                                <OpenAPIRootSchema schema={schema} context={clientContext} />
-                            </SectionBody>
-                        </Section>
-                    </OpenAPIDisclosure>
+                        name={name}
+                        context={clientContext}
+                        schema={schema}
+                    />
                 );
             })}
         </div>
diff --git a/packages/react-openapi/src/translations/de.ts b/packages/react-openapi/src/translations/de.ts
index 8fc792e8ea..6229acd4fa 100644
--- a/packages/react-openapi/src/translations/de.ts
+++ b/packages/react-openapi/src/translations/de.ts
@@ -18,9 +18,11 @@ export const de = {
     nullable: 'Nullfähig',
     body: 'Rumpf',
     payload: 'Nutzlast',
-    headers: 'Kopfzeilen',
+    headers: 'Header',
+    header: 'Header',
     authorizations: 'Autorisierungen',
     responses: 'Antworten',
+    response: 'Antwort',
     path_parameters: 'Pfadparameter',
     query_parameters: 'Abfrageparameter',
     header_parameters: 'Header-Parameter',
@@ -30,8 +32,12 @@ export const de = {
     success: 'Erfolg',
     redirect: 'Umleitung',
     error: 'Fehler',
-    show: 'Anzeigen',
-    hide: 'Verstecken',
+    show: 'Zeige ${1}',
+    hide: 'Verstecke ${1}',
     available_items: 'Verfügbare Elemente',
-    child_attributes: 'Unterattribute',
+    available_scopes: 'Verfügbare scopes',
+    properties: 'Eigenschaften',
+    or: 'oder',
+    and: 'und',
+    possible_values: 'Mögliche Werte',
 };
diff --git a/packages/react-openapi/src/translations/en.ts b/packages/react-openapi/src/translations/en.ts
index 61f45e9e3f..3681626898 100644
--- a/packages/react-openapi/src/translations/en.ts
+++ b/packages/react-openapi/src/translations/en.ts
@@ -19,8 +19,10 @@ export const en = {
     body: 'Body',
     payload: 'Payload',
     headers: 'Headers',
+    header: 'Header',
     authorizations: 'Authorizations',
     responses: 'Responses',
+    response: 'Response',
     path_parameters: 'Path parameters',
     query_parameters: 'Query parameters',
     header_parameters: 'Header parameters',
@@ -30,8 +32,12 @@ export const en = {
     success: 'Success',
     redirect: 'Redirect',
     error: 'Error',
-    show: 'Show',
-    hide: 'Hide',
+    show: 'Show ${1}',
+    hide: 'Hide ${1}',
     available_items: 'Available items',
-    child_attributes: 'Child attributes',
+    available_scopes: 'Available scopes',
+    possible_values: 'Possible values',
+    properties: 'Properties',
+    or: 'or',
+    and: 'and',
 };
diff --git a/packages/react-openapi/src/translations/es.ts b/packages/react-openapi/src/translations/es.ts
index 5faa36c674..f9e8c58f48 100644
--- a/packages/react-openapi/src/translations/es.ts
+++ b/packages/react-openapi/src/translations/es.ts
@@ -18,9 +18,11 @@ export const es = {
     nullable: 'Nulo',
     body: 'Cuerpo',
     payload: 'Caga útil',
-    headers: 'Encabezados',
+    headers: 'Headers',
+    header: 'Header',
     authorizations: 'Autorizaciones',
     responses: 'Respuestas',
+    response: 'Respuesta',
     path_parameters: 'Parámetros de ruta',
     query_parameters: 'Parámetros de consulta',
     header_parameters: 'Parámetros de encabezado',
@@ -30,8 +32,12 @@ export const es = {
     success: 'Éxito',
     redirect: 'Redirección',
     error: 'Error',
-    show: 'Mostrar',
-    hide: 'Ocultar',
+    show: 'Mostrar ${1}',
+    hide: 'Ocultar ${1}',
     available_items: 'Elementos disponibles',
-    child_attributes: 'Atributos secundarios',
+    available_scopes: 'Scopes disponibles',
+    properties: 'Propiedades',
+    or: 'o',
+    and: 'y',
+    possible_values: 'Valores posibles',
 };
diff --git a/packages/react-openapi/src/translations/fr.ts b/packages/react-openapi/src/translations/fr.ts
index ccaf05b3d9..fde7a9222c 100644
--- a/packages/react-openapi/src/translations/fr.ts
+++ b/packages/react-openapi/src/translations/fr.ts
@@ -18,20 +18,26 @@ export const fr = {
     nullable: 'Nullable',
     body: 'Corps',
     payload: 'Charge utile',
-    headers: 'En-têtes',
+    headers: 'Headers',
+    header: 'Header',
     authorizations: 'Autorisations',
     responses: 'Réponses',
+    response: 'Réponse',
     path_parameters: 'Paramètres de chemin',
     query_parameters: 'Paramètres de requête',
-    header_parameters: 'Paramètres d’en-tête',
+    header_parameters: "Paramètres d'en-tête",
     attributes: 'Attributs',
     test_it: 'Tester',
     information: 'Information',
     success: 'Succès',
     redirect: 'Redirection',
     error: 'Erreur',
-    show: 'Afficher',
-    hide: 'Masquer',
+    show: 'Afficher ${1}',
+    hide: 'Masquer ${1}',
     available_items: 'Éléments disponibles',
-    child_attributes: 'Attributs enfants',
+    available_scopes: 'Scopes disponibles',
+    properties: 'Propriétés',
+    or: 'ou',
+    and: 'et',
+    possible_values: 'Valeurs possibles',
 };
diff --git a/packages/react-openapi/src/translations/ja.ts b/packages/react-openapi/src/translations/ja.ts
index 55b5b2a0a0..04d43f67ae 100644
--- a/packages/react-openapi/src/translations/ja.ts
+++ b/packages/react-openapi/src/translations/ja.ts
@@ -19,8 +19,10 @@ export const ja = {
     body: '本文',
     payload: 'ペイロード',
     headers: 'ヘッダー',
+    header: 'ヘッダー',
     authorizations: '認可',
     responses: 'レスポンス',
+    response: 'レスポンス',
     path_parameters: 'パスパラメータ',
     query_parameters: 'クエリパラメータ',
     header_parameters: 'ヘッダーパラメータ',
@@ -30,8 +32,12 @@ export const ja = {
     success: '成功',
     redirect: 'リダイレクト',
     error: 'エラー',
-    show: '表示',
-    hide: '非表示',
+    show: '${1}を表示',
+    hide: '${1}を非表示',
     available_items: '利用可能なアイテム',
-    child_attributes: '子属性',
+    available_scopes: '利用可能なスコープ',
+    properties: 'プロパティ',
+    or: 'または',
+    and: 'かつ',
+    possible_values: '可能な値',
 };
diff --git a/packages/react-openapi/src/translations/nl.ts b/packages/react-openapi/src/translations/nl.ts
index 5186580c78..2c57d7af4f 100644
--- a/packages/react-openapi/src/translations/nl.ts
+++ b/packages/react-openapi/src/translations/nl.ts
@@ -19,8 +19,10 @@ export const nl = {
     body: 'Body',
     payload: 'Payload',
     headers: 'Headers',
+    header: 'Header',
     authorizations: 'Autorisaties',
     responses: 'Reacties',
+    response: 'Reactie',
     path_parameters: 'Padparameters',
     query_parameters: 'Queryparameters',
     header_parameters: 'Headerparameters',
@@ -30,8 +32,12 @@ export const nl = {
     success: 'Succes',
     redirect: 'Omleiding',
     error: 'Fout',
-    show: 'Toon',
-    hide: 'Verbergen',
+    show: 'Toon ${1}',
+    hide: 'Verberg ${1}',
     available_items: 'Beschikbare items',
-    child_attributes: 'Kindattributen',
+    available_scopes: 'Beschikbare scopes',
+    properties: 'Eigenschappen',
+    or: 'of',
+    and: 'en',
+    possible_values: 'Mogelijke waarden',
 };
diff --git a/packages/react-openapi/src/translations/no.ts b/packages/react-openapi/src/translations/no.ts
index a4efc3cfe7..9ef1b80048 100644
--- a/packages/react-openapi/src/translations/no.ts
+++ b/packages/react-openapi/src/translations/no.ts
@@ -18,9 +18,11 @@ export const no = {
     nullable: 'Kan være null',
     body: 'Brødtekst',
     payload: 'Nyttelast',
-    headers: 'Overskrifter',
+    headers: 'Headers',
+    header: 'Header',
     authorizations: 'Autorisasjoner',
     responses: 'Responser',
+    response: 'Respons',
     path_parameters: 'Sti-parametere',
     query_parameters: 'Forespørselsparametere',
     header_parameters: 'Header-parametere',
@@ -30,8 +32,12 @@ export const no = {
     success: 'Suksess',
     redirect: 'Viderekobling',
     error: 'Feil',
-    show: 'Vis',
-    hide: 'Skjul',
+    show: 'Vis ${1}',
+    hide: 'Skjul ${1}',
     available_items: 'Tilgjengelige elementer',
-    child_attributes: 'Barneattributter',
+    available_scopes: 'Tilgjengelige scopes',
+    properties: 'Egenskaper',
+    or: 'eller',
+    and: 'og',
+    possible_values: 'Mulige verdier',
 };
diff --git a/packages/react-openapi/src/translations/pt-br.ts b/packages/react-openapi/src/translations/pt-br.ts
index 7c1f86a7c0..2e9e7cb2d9 100644
--- a/packages/react-openapi/src/translations/pt-br.ts
+++ b/packages/react-openapi/src/translations/pt-br.ts
@@ -18,9 +18,11 @@ export const pt_br = {
     nullable: 'Nulo',
     body: 'Corpo',
     payload: 'Carga útil',
-    headers: 'Cabeçalhos',
+    headers: 'Headers',
+    header: 'Header',
     authorizations: 'Autorizações',
     responses: 'Respostas',
+    response: 'Resposta',
     path_parameters: 'Parâmetros de rota',
     query_parameters: 'Parâmetros de consulta',
     header_parameters: 'Parâmetros de cabeçalho',
@@ -30,8 +32,12 @@ export const pt_br = {
     success: 'Sucesso',
     redirect: 'Redirecionamento',
     error: 'Erro',
-    show: 'Mostrar',
-    hide: 'Ocultar',
+    show: 'Mostrar ${1}',
+    hide: 'Ocultar ${1}',
     available_items: 'Itens disponíveis',
-    child_attributes: 'Atributos filhos',
+    available_scopes: 'Scopes disponíveis',
+    properties: 'Propriedades',
+    or: 'ou',
+    and: 'e',
+    possible_values: 'Valores possíveis',
 };
diff --git a/packages/react-openapi/src/translations/zh.ts b/packages/react-openapi/src/translations/zh.ts
index 414043fd32..f0e81f21bc 100644
--- a/packages/react-openapi/src/translations/zh.ts
+++ b/packages/react-openapi/src/translations/zh.ts
@@ -18,9 +18,11 @@ export const zh = {
     nullable: '可为 null',
     body: '请求体',
     payload: '有效载荷',
-    headers: '头部信息',
+    headers: '头字段',
+    header: '头部',
     authorizations: '授权',
     responses: '响应',
+    response: '响应',
     path_parameters: '路径参数',
     query_parameters: '查询参数',
     header_parameters: '头参数',
@@ -30,8 +32,12 @@ export const zh = {
     success: '成功',
     redirect: '重定向',
     error: '错误',
-    show: '显示',
-    hide: '隐藏',
+    show: '显示${1}',
+    hide: '隐藏${1}',
     available_items: '可用项',
-    child_attributes: '子属性',
+    available_scopes: '可用范围',
+    properties: '属性',
+    or: '或',
+    and: '和',
+    possible_values: '可能的值',
 };