diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index dbcc8975f8..9760956873 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -51,6 +51,8 @@ module.exports = {
       },
       rules: {
         '@typescript-eslint/no-floating-promises': 'error',
+        'no-use-before-define': 'off',
+        '@typescript-eslint/no-use-before-define': 'error',
       },
     },
     {
diff --git a/src/build/advanced-api-routes.ts b/src/build/advanced-api-routes.ts
index 6cd024424f..6e1c45f7a7 100644
--- a/src/build/advanced-api-routes.ts
+++ b/src/build/advanced-api-routes.ts
@@ -35,49 +35,6 @@ interface ApiBackgroundConfig {
 
 type ApiConfig = ApiStandardConfig | ApiScheduledConfig | ApiBackgroundConfig
 
-export async function getAPIRoutesConfigs(ctx: PluginContext) {
-  const functionsConfigManifestPath = join(
-    ctx.publishDir,
-    'server',
-    'functions-config-manifest.json',
-  )
-  if (!existsSync(functionsConfigManifestPath)) {
-    // before https://github.com/vercel/next.js/pull/60163 this file might not have been produced if there were no API routes at all
-    return []
-  }
-
-  const functionsConfigManifest = JSON.parse(
-    await readFile(functionsConfigManifestPath, 'utf-8'),
-  ) as FunctionsConfigManifest
-
-  const appDir = ctx.resolveFromSiteDir('.')
-  const pagesDir = join(appDir, 'pages')
-  const srcPagesDir = join(appDir, 'src', 'pages')
-  const { pageExtensions } = ctx.requiredServerFiles.config
-
-  return Promise.all(
-    Object.keys(functionsConfigManifest.functions).map(async (apiRoute) => {
-      const filePath = getSourceFileForPage(apiRoute, [pagesDir, srcPagesDir], pageExtensions)
-
-      const sharedFields = {
-        apiRoute,
-        filePath,
-        config: {} as ApiConfig,
-      }
-
-      if (filePath) {
-        const config = await extractConfigFromFile(filePath, appDir)
-        return {
-          ...sharedFields,
-          config,
-        }
-      }
-
-      return sharedFields
-    }),
-  )
-}
-
 // Next.js already defines a default `pageExtensions` array in its `required-server-files.json` file
 // In case it gets `undefined`, this is a fallback
 const SOURCE_FILE_EXTENSIONS = ['js', 'jsx', 'ts', 'tsx']
@@ -186,3 +143,46 @@ const extractConfigFromFile = async (apiFilePath: string, appDir: string): Promi
     return {}
   }
 }
+
+export async function getAPIRoutesConfigs(ctx: PluginContext) {
+  const functionsConfigManifestPath = join(
+    ctx.publishDir,
+    'server',
+    'functions-config-manifest.json',
+  )
+  if (!existsSync(functionsConfigManifestPath)) {
+    // before https://github.com/vercel/next.js/pull/60163 this file might not have been produced if there were no API routes at all
+    return []
+  }
+
+  const functionsConfigManifest = JSON.parse(
+    await readFile(functionsConfigManifestPath, 'utf-8'),
+  ) as FunctionsConfigManifest
+
+  const appDir = ctx.resolveFromSiteDir('.')
+  const pagesDir = join(appDir, 'pages')
+  const srcPagesDir = join(appDir, 'src', 'pages')
+  const { pageExtensions } = ctx.requiredServerFiles.config
+
+  return Promise.all(
+    Object.keys(functionsConfigManifest.functions).map(async (apiRoute) => {
+      const filePath = getSourceFileForPage(apiRoute, [pagesDir, srcPagesDir], pageExtensions)
+
+      const sharedFields = {
+        apiRoute,
+        filePath,
+        config: {} as ApiConfig,
+      }
+
+      if (filePath) {
+        const config = await extractConfigFromFile(filePath, appDir)
+        return {
+          ...sharedFields,
+          config,
+        }
+      }
+
+      return sharedFields
+    }),
+  )
+}
diff --git a/src/build/content/prerendered.ts b/src/build/content/prerendered.ts
index 9f1ae2fe75..a05f023bef 100644
--- a/src/build/content/prerendered.ts
+++ b/src/build/content/prerendered.ts
@@ -1,5 +1,5 @@
 import { existsSync } from 'node:fs'
-import { mkdir, readFile, writeFile } from 'node:fs/promises'
+import { mkdir, readFile } from 'node:fs/promises'
 import { join } from 'node:path'
 
 import { trace } from '@opentelemetry/api'
@@ -8,7 +8,6 @@ import { glob } from 'fast-glob'
 import pLimit from 'p-limit'
 import { satisfies } from 'semver'
 
-import { encodeBlobKey } from '../../shared/blobkey.js'
 import type {
   CachedFetchValue,
   NetlifyCachedAppPageValue,
@@ -31,13 +30,11 @@ const writeCacheEntry = async (
   lastModified: number,
   ctx: PluginContext,
 ): Promise<void> => {
-  const path = join(ctx.blobDir, await encodeBlobKey(route))
   const entry = JSON.stringify({
     lastModified,
     value,
   } satisfies NetlifyCacheHandlerValue)
-
-  await writeFile(path, entry, 'utf-8')
+  await ctx.setBlob(route, entry)
 }
 
 /**
diff --git a/src/build/content/server.ts b/src/build/content/server.ts
index ab7f4fc3cf..6cc58ee63f 100644
--- a/src/build/content/server.ts
+++ b/src/build/content/server.ts
@@ -29,6 +29,30 @@ function isError(error: unknown): error is NodeJS.ErrnoException {
   return error instanceof Error
 }
 
+/**
+ * Generates a copy of the middleware manifest without any middleware in it. We
+ * do this because we'll run middleware in an edge function, and we don't want
+ * to run it again in the server handler.
+ */
+const replaceMiddlewareManifest = async (sourcePath: string, destPath: string) => {
+  await mkdir(dirname(destPath), { recursive: true })
+
+  const data = await readFile(sourcePath, 'utf8')
+  const manifest = JSON.parse(data)
+
+  // TODO: Check for `manifest.version` and write an error to the system log
+  // when we find a value that is not equal to 2. This will alert us in case
+  // Next.js starts using a new format for the manifest and we're writing
+  // one with the old version.
+  const newManifest = {
+    ...manifest,
+    middleware: {},
+  }
+  const newData = JSON.stringify(newManifest)
+
+  await writeFile(destPath, newData)
+}
+
 /**
  * Copy App/Pages Router Javascript needed by the server handler
  */
@@ -311,30 +335,6 @@ export const copyNextDependencies = async (ctx: PluginContext): Promise<void> =>
   })
 }
 
-/**
- * Generates a copy of the middleware manifest without any middleware in it. We
- * do this because we'll run middleware in an edge function, and we don't want
- * to run it again in the server handler.
- */
-const replaceMiddlewareManifest = async (sourcePath: string, destPath: string) => {
-  await mkdir(dirname(destPath), { recursive: true })
-
-  const data = await readFile(sourcePath, 'utf8')
-  const manifest = JSON.parse(data)
-
-  // TODO: Check for `manifest.version` and write an error to the system log
-  // when we find a value that is not equal to 2. This will alert us in case
-  // Next.js starts using a new format for the manifest and we're writing
-  // one with the old version.
-  const newManifest = {
-    ...manifest,
-    middleware: {},
-  }
-  const newData = JSON.stringify(newManifest)
-
-  await writeFile(destPath, newData)
-}
-
 export const verifyHandlerDirStructure = async (ctx: PluginContext) => {
   const runConfig = JSON.parse(await readFile(join(ctx.serverHandlerDir, RUN_CONFIG), 'utf-8'))
 
diff --git a/src/build/content/static.ts b/src/build/content/static.ts
index 4079695bd4..a155e76206 100644
--- a/src/build/content/static.ts
+++ b/src/build/content/static.ts
@@ -1,12 +1,11 @@
 import { existsSync } from 'node:fs'
-import { cp, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
+import { cp, mkdir, readFile, rename, rm } from 'node:fs/promises'
 import { basename, join } from 'node:path'
 
 import { trace } from '@opentelemetry/api'
 import { wrapTracer } from '@opentelemetry/api/experimental'
 import glob from 'fast-glob'
 
-import { encodeBlobKey } from '../../shared/blobkey.js'
 import { PluginContext } from '../plugin-context.js'
 import { verifyNetlifyForms } from '../verification.js'
 
@@ -33,7 +32,7 @@ export const copyStaticContent = async (ctx: PluginContext): Promise<void> => {
           .map(async (path): Promise<void> => {
             const html = await readFile(join(srcDir, path), 'utf-8')
             verifyNetlifyForms(ctx, html)
-            await writeFile(join(destDir, await encodeBlobKey(path)), html, 'utf-8')
+            await ctx.setBlob(path, html)
           }),
       )
     } catch (error) {
diff --git a/src/build/functions/edge.ts b/src/build/functions/edge.ts
index 4c95ad1353..51520645a8 100644
--- a/src/build/functions/edge.ts
+++ b/src/build/functions/edge.ts
@@ -1,7 +1,7 @@
 import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
 import { dirname, join } from 'node:path'
 
-import type { Manifest, ManifestFunction } from '@netlify/edge-functions'
+import type { IntegrationsConfig, Manifest, ManifestFunction } from '@netlify/edge-functions'
 import { glob } from 'fast-glob'
 import type { EdgeFunctionDefinition as NextDefinition } from 'next/dist/build/webpack/plugins/middleware-plugin.js'
 import { pathToRegexp } from 'path-to-regexp'
@@ -53,7 +53,23 @@ const augmentMatchers = (
   })
 }
 
-const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefinition) => {
+const getHandlerName = ({ name }: Pick<NextDefinition, 'name'>): string =>
+  `${EDGE_HANDLER_NAME}-${name.replace(/\W/g, '-')}`
+
+const getEdgeFunctionSharedConfig = (
+  ctx: PluginContext,
+  { name, page }: Pick<NextDefinition, 'name' | 'page'>,
+) => {
+  return {
+    name: name.endsWith('middleware')
+      ? 'Next.js Middleware Handler'
+      : `Next.js Edge Handler: ${page}`,
+    cache: name.endsWith('middleware') ? undefined : ('manual' as const),
+    generator: `${ctx.pluginName}@${ctx.pluginVersion}`,
+  }
+}
+
+const writeHandlerFile = async (ctx: PluginContext, { matchers, name, page }: NextDefinition) => {
   const nextConfig = ctx.buildConfig
   const handlerName = getHandlerName({ name })
   const handlerDirectory = join(ctx.edgeFunctionsDir, handlerName)
@@ -63,6 +79,8 @@ const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefi
   // Netlify Edge Functions and the Next.js edge runtime.
   await copyRuntime(ctx, handlerDirectory)
 
+  const augmentedMatchers = augmentMatchers(matchers, ctx)
+
   // Writing a file with the matchers that should trigger this function. We'll
   // read this file from the function at runtime.
   await writeFile(join(handlerRuntimeDirectory, 'matchers.json'), JSON.stringify(matchers))
@@ -82,6 +100,14 @@ const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefi
     JSON.stringify(minimalNextConfig),
   )
 
+  const isc =
+    ctx.edgeFunctionsConfigStrategy === 'inline'
+      ? `export const config = ${JSON.stringify({
+          ...getEdgeFunctionSharedConfig(ctx, { name, page }),
+          pattern: augmentedMatchers.map((matcher) => matcher.regexp),
+        } satisfies IntegrationsConfig)};`
+      : ``
+
   // Writing the function entry file. It wraps the middleware code with the
   // compatibility layer mentioned above.
   await writeFile(
@@ -90,7 +116,7 @@ const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefi
     import {handleMiddleware} from './edge-runtime/middleware.ts';
     import handler from './server/${name}.js';
     export default (req, context) => handleMiddleware(req, context, handler);
-    `,
+    ${isc}`,
   )
 }
 
@@ -136,26 +162,16 @@ const createEdgeHandler = async (ctx: PluginContext, definition: NextDefinition)
   await writeHandlerFile(ctx, definition)
 }
 
-const getHandlerName = ({ name }: Pick<NextDefinition, 'name'>): string =>
-  `${EDGE_HANDLER_NAME}-${name.replace(/\W/g, '-')}`
-
 const buildHandlerDefinition = (
   ctx: PluginContext,
   { name, matchers, page }: NextDefinition,
 ): Array<ManifestFunction> => {
   const fun = getHandlerName({ name })
-  const funName = name.endsWith('middleware')
-    ? 'Next.js Middleware Handler'
-    : `Next.js Edge Handler: ${page}`
-  const cache = name.endsWith('middleware') ? undefined : ('manual' as const)
-  const generator = `${ctx.pluginName}@${ctx.pluginVersion}`
 
   return augmentMatchers(matchers, ctx).map((matcher) => ({
+    ...getEdgeFunctionSharedConfig(ctx, { name, page }),
     function: fun,
-    name: funName,
     pattern: matcher.regexp,
-    cache,
-    generator,
   }))
 }
 
@@ -171,10 +187,12 @@ export const createEdgeHandlers = async (ctx: PluginContext) => {
   ]
   await Promise.all(nextDefinitions.map((def) => createEdgeHandler(ctx, def)))
 
-  const netlifyDefinitions = nextDefinitions.flatMap((def) => buildHandlerDefinition(ctx, def))
-  const netlifyManifest: Manifest = {
-    version: 1,
-    functions: netlifyDefinitions,
+  if (ctx.edgeFunctionsConfigStrategy === 'manifest') {
+    const netlifyDefinitions = nextDefinitions.flatMap((def) => buildHandlerDefinition(ctx, def))
+    const netlifyManifest: Manifest = {
+      version: 1,
+      functions: netlifyDefinitions,
+    }
+    await writeEdgeManifest(ctx, netlifyManifest)
   }
-  await writeEdgeManifest(ctx, netlifyManifest)
 }
diff --git a/src/build/functions/server.ts b/src/build/functions/server.ts
index bd38a82162..cd477be5c6 100644
--- a/src/build/functions/server.ts
+++ b/src/build/functions/server.ts
@@ -105,7 +105,9 @@ const getHandlerFile = async (ctx: PluginContext): Promise<string> => {
   const templatesDir = join(ctx.pluginDir, 'dist/build/templates')
 
   const templateVariables: Record<string, string> = {
-    '{{useRegionalBlobs}}': ctx.useRegionalBlobs.toString(),
+    '{{useRegionalBlobs}}': (ctx.blobsStrategy !== 'legacy').toString(),
+    '{{generator}}': `${ctx.pluginName}@${ctx.pluginVersion}`,
+    '{{serverHandlerRootDir}}': ctx.serverHandlerRootDir,
   }
   // In this case it is a monorepo and we need to use a own template for it
   // as we have to change the process working directory
@@ -143,7 +145,9 @@ export const createServerHandler = async (ctx: PluginContext) => {
     await copyNextServerCode(ctx)
     await copyNextDependencies(ctx)
     await copyHandlerDependencies(ctx)
-    await writeHandlerManifest(ctx)
+    if (ctx.serverHandlerConfigStrategy === 'manifest') {
+      await writeHandlerManifest(ctx)
+    }
     await writeHandlerFile(ctx)
 
     await verifyHandlerDirStructure(ctx)
diff --git a/src/build/plugin-context.test.ts b/src/build/plugin-context.test.ts
index 5c18c2a5a2..863538fed3 100644
--- a/src/build/plugin-context.test.ts
+++ b/src/build/plugin-context.test.ts
@@ -211,3 +211,20 @@ test('should use deploy configuration blobs directory when @netlify/build versio
 
   expect(ctx.blobDir).toBe(join(cwd, '.netlify/deploy/v1/blobs/deploy'))
 })
+
+test('should use frameworks API directories when @netlify/build version supports it', () => {
+  const { cwd } = mockFileSystem({
+    '.next/required-server-files.json': JSON.stringify({
+      config: { distDir: '.next' },
+      relativeAppDir: '',
+    } as RequiredServerFilesManifest),
+  })
+
+  const ctx = new PluginContext({
+    constants: { NETLIFY_BUILD_VERSION: '29.50.5' },
+  } as unknown as NetlifyPluginOptions)
+
+  expect(ctx.blobDir).toBe(join(cwd, '.netlify/v1/blobs/deploy'))
+  expect(ctx.edgeFunctionsDir).toBe(join(cwd, '.netlify/v1/edge-functions'))
+  expect(ctx.serverFunctionsDir).toBe(join(cwd, '.netlify/v1/functions'))
+})
diff --git a/src/build/plugin-context.ts b/src/build/plugin-context.ts
index 9b0ecc199c..a0902c7d3f 100644
--- a/src/build/plugin-context.ts
+++ b/src/build/plugin-context.ts
@@ -1,7 +1,7 @@
 import { existsSync, readFileSync } from 'node:fs'
-import { readFile } from 'node:fs/promises'
+import { mkdir, readFile, writeFile } from 'node:fs/promises'
 import { createRequire } from 'node:module'
-import { join, relative, resolve } from 'node:path'
+import { dirname, join, relative, resolve } from 'node:path'
 import { join as posixJoin } from 'node:path/posix'
 import { fileURLToPath } from 'node:url'
 
@@ -15,6 +15,8 @@ import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middlew
 import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
 import { satisfies } from 'semver'
 
+import { encodeBlobKey } from '../shared/blobkey.js'
+
 const MODULE_DIR = fileURLToPath(new URL('.', import.meta.url))
 const PLUGIN_DIR = join(MODULE_DIR, '../..')
 const DEFAULT_PUBLISH_DIR = '.next'
@@ -137,36 +139,85 @@ export class PluginContext {
 
   /**
    * Absolute path of the directory that will be deployed to the blob store
+   * frameworks api: `.netlify/v1/blobs/deploy`
    * region aware: `.netlify/deploy/v1/blobs/deploy`
-   * default: `.netlify/blobs/deploy`
+   * legacy/default: `.netlify/blobs/deploy`
    */
   get blobDir(): string {
-    if (this.useRegionalBlobs) {
-      return this.resolveFromPackagePath('.netlify/deploy/v1/blobs/deploy')
+    switch (this.blobsStrategy) {
+      case 'frameworks-api':
+        return this.resolveFromPackagePath('.netlify/v1/blobs/deploy')
+      case 'regional':
+        return this.resolveFromPackagePath('.netlify/deploy/v1/blobs/deploy')
+      case 'legacy':
+      default:
+        return this.resolveFromPackagePath('.netlify/blobs/deploy')
     }
+  }
 
-    return this.resolveFromPackagePath('.netlify/blobs/deploy')
+  async setBlob(key: string, value: string) {
+    switch (this.blobsStrategy) {
+      case 'frameworks-api': {
+        const path = join(this.blobDir, await encodeBlobKey(key), 'blob')
+        await mkdir(dirname(path), { recursive: true })
+        await writeFile(path, value, 'utf-8')
+        return
+      }
+      case 'regional':
+      case 'legacy':
+      default: {
+        const path = join(this.blobDir, await encodeBlobKey(key))
+        await writeFile(path, value, 'utf-8')
+      }
+    }
   }
 
   get buildVersion(): string {
     return this.constants.NETLIFY_BUILD_VERSION || 'v0.0.0'
   }
 
-  get useRegionalBlobs(): boolean {
-    if (!(this.featureFlags || {})['next-runtime-regional-blobs']) {
-      return false
+  #useFrameworksAPI: PluginContext['useFrameworksAPI'] | null = null
+  get useFrameworksAPI(): boolean {
+    if (this.#useFrameworksAPI === null) {
+      // Defining RegExp pattern in edge function inline config is only supported since Build 29.50.5 / CLI 17.32.1
+      const REQUIRED_BUILD_VERSION = '>=29.50.5'
+      this.#useFrameworksAPI = satisfies(this.buildVersion, REQUIRED_BUILD_VERSION, {
+        includePrerelease: true,
+      })
+    }
+
+    return this.#useFrameworksAPI
+  }
+
+  #blobsStrategy: PluginContext['blobsStrategy'] | null = null
+  get blobsStrategy(): 'legacy' | 'regional' | 'frameworks-api' {
+    if (this.#blobsStrategy === null) {
+      if (this.useFrameworksAPI) {
+        this.#blobsStrategy = 'frameworks-api'
+      } else {
+        // Region-aware blobs are only available as of CLI v17.23.5 (i.e. Build v29.41.5)
+        const REQUIRED_BUILD_VERSION = '>=29.41.5'
+        this.#blobsStrategy = satisfies(this.buildVersion, REQUIRED_BUILD_VERSION, {
+          includePrerelease: true,
+        })
+          ? 'regional'
+          : 'legacy'
+      }
     }
 
-    // Region-aware blobs are only available as of CLI v17.23.5 (i.e. Build v29.41.5)
-    const REQUIRED_BUILD_VERSION = '>=29.41.5'
-    return satisfies(this.buildVersion, REQUIRED_BUILD_VERSION, { includePrerelease: true })
+    return this.#blobsStrategy
   }
 
   /**
    * Absolute path of the directory containing the files for the serverless lambda function
-   * `.netlify/functions-internal`
+   * frameworks api: `.netlify/v1/functions`
+   * legacy/default: `.netlify/functions-internal`
    */
   get serverFunctionsDir(): string {
+    if (this.useFrameworksAPI) {
+      return this.resolveFromPackagePath('.netlify/v1/functions')
+    }
+
     return this.resolveFromPackagePath('.netlify/functions-internal')
   }
 
@@ -193,14 +244,27 @@ export class PluginContext {
     return './.netlify/dist/run/handlers/server.js'
   }
 
+  get serverHandlerConfigStrategy(): 'manifest' | 'inline' {
+    return this.useFrameworksAPI ? 'inline' : 'manifest'
+  }
+
   /**
    * Absolute path of the directory containing the files for deno edge functions
-   * `.netlify/edge-functions`
+   * frameworks api: `.netlify/v1/edge-functions`
+   * legacy/default: `.netlify/edge-functions`
    */
   get edgeFunctionsDir(): string {
+    if (this.useFrameworksAPI) {
+      return this.resolveFromPackagePath('.netlify/v1/edge-functions')
+    }
+
     return this.resolveFromPackagePath('.netlify/edge-functions')
   }
 
+  get edgeFunctionsConfigStrategy(): 'manifest' | 'inline' {
+    return this.useFrameworksAPI ? 'inline' : 'manifest'
+  }
+
   /** Absolute path of the edge handler */
   get edgeHandlerDir(): string {
     return join(this.edgeFunctionsDir, EDGE_HANDLER_NAME)
diff --git a/src/build/templates/handler-monorepo.tmpl.js b/src/build/templates/handler-monorepo.tmpl.js
index 6e9b9a9a56..a13a3f294a 100644
--- a/src/build/templates/handler-monorepo.tmpl.js
+++ b/src/build/templates/handler-monorepo.tmpl.js
@@ -53,4 +53,9 @@ export default async function (req, context) {
 export const config = {
   path: '/*',
   preferStatic: true,
+  name: 'Next.js Server Handler',
+  generator: '{{generator}}',
+  nodeBundler: 'none',
+  includedFiles: ['**'],
+  includedFilesBasePath: '{{serverHandlerRootDir}}',
 }
diff --git a/src/build/templates/handler.tmpl.js b/src/build/templates/handler.tmpl.js
index 0b10bcd902..3381b69651 100644
--- a/src/build/templates/handler.tmpl.js
+++ b/src/build/templates/handler.tmpl.js
@@ -46,4 +46,9 @@ export default async function handler(req, context) {
 export const config = {
   path: '/*',
   preferStatic: true,
+  name: 'Next.js Server Handler',
+  generator: '{{generator}}',
+  nodeBundler: 'none',
+  includedFiles: ['**'],
+  includedFilesBasePath: '{{serverHandlerRootDir}}',
 }
diff --git a/src/index.ts b/src/index.ts
index 219a554615..2fb10da678 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -58,6 +58,15 @@ export const onBuild = async (options: NetlifyPluginOptions) => {
     verifyPublishDir(ctx)
 
     span.setAttribute('next.buildConfig', JSON.stringify(ctx.buildConfig))
+    span.setAttribute(
+      'next.deployStrategy',
+      JSON.stringify({
+        useFrameworksAPI: ctx.useFrameworksAPI,
+        blobsStrategy: ctx.blobsStrategy,
+        edgeFunctionsConfigStrategy: ctx.edgeFunctionsConfigStrategy,
+        serverHandlerConfigStrategy: ctx.serverHandlerConfigStrategy,
+      }),
+    )
 
     // only save the build cache if not run via the CLI
     if (!options.constants.IS_LOCAL) {
diff --git a/tests/e2e/cli-before-frameworks-api-support.test.ts b/tests/e2e/cli-before-frameworks-api-support.test.ts
new file mode 100644
index 0000000000..a46a41991c
--- /dev/null
+++ b/tests/e2e/cli-before-frameworks-api-support.test.ts
@@ -0,0 +1,28 @@
+import { expect } from '@playwright/test'
+import { test } from '../utils/playwright-helpers.js'
+
+test('should serve 404 page when requesting non existing page (no matching route) if site is deployed with CLI not supporting frameworks API', async ({
+  page,
+  cliBeforeFrameworksAPISupport,
+}) => {
+  // 404 page is built and uploaded to blobs at build time
+  // when Next.js serves 404 it will try to fetch it from the blob store
+  // if request handler function is unable to get from blob store it will
+  // fail request handling and serve 500 error.
+  // This implicitly tests that request handler function is able to read blobs
+  // that are uploaded as part of site deploy.
+  // This also tests if edge middleware is working.
+
+  const response = await page.goto(new URL('non-existing', cliBeforeFrameworksAPISupport.url).href)
+  const headers = response?.headers() || {}
+  expect(response?.status()).toBe(404)
+
+  expect(await page.textContent('h1')).toBe('404')
+
+  expect(headers['netlify-cdn-cache-control']).toBe(
+    'no-cache, no-store, max-age=0, must-revalidate, durable',
+  )
+  expect(headers['cache-control']).toBe('no-cache,no-store,max-age=0,must-revalidate')
+
+  expect(headers['x-hello-from-middleware']).toBe('hello')
+})
diff --git a/tests/fixtures/cli-before-frameworks-api-support/middleware.ts b/tests/fixtures/cli-before-frameworks-api-support/middleware.ts
new file mode 100644
index 0000000000..3b40120cab
--- /dev/null
+++ b/tests/fixtures/cli-before-frameworks-api-support/middleware.ts
@@ -0,0 +1,9 @@
+import { NextResponse } from 'next/server'
+
+export function middleware() {
+  const response: NextResponse = NextResponse.next()
+
+  response.headers.set('x-hello-from-middleware', 'hello')
+
+  return response
+}
diff --git a/tests/fixtures/cli-before-frameworks-api-support/next.config.js b/tests/fixtures/cli-before-frameworks-api-support/next.config.js
new file mode 100644
index 0000000000..8d2a9bf37a
--- /dev/null
+++ b/tests/fixtures/cli-before-frameworks-api-support/next.config.js
@@ -0,0 +1,8 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+  eslint: {
+    ignoreDuringBuilds: true,
+  },
+}
+
+module.exports = nextConfig
diff --git a/tests/fixtures/cli-before-frameworks-api-support/package.json b/tests/fixtures/cli-before-frameworks-api-support/package.json
new file mode 100644
index 0000000000..530a6c70ce
--- /dev/null
+++ b/tests/fixtures/cli-before-frameworks-api-support/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "old-cli",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "postinstall": "next build",
+    "dev": "next dev",
+    "build": "next build"
+  },
+  "dependencies": {
+    "next": "latest",
+    "netlify-cli": "17.32.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
+  }
+}
diff --git a/tests/fixtures/cli-before-frameworks-api-support/pages/index.js b/tests/fixtures/cli-before-frameworks-api-support/pages/index.js
new file mode 100644
index 0000000000..70acbeca65
--- /dev/null
+++ b/tests/fixtures/cli-before-frameworks-api-support/pages/index.js
@@ -0,0 +1,15 @@
+export default function Home({ ssr }) {
+  return (
+    <main>
+      <div data-testid="smoke">SSR: {ssr ? 'yes' : 'no'}</div>
+    </main>
+  )
+}
+
+export const getServerSideProps = async () => {
+  return {
+    props: {
+      ssr: true,
+    },
+  }
+}
diff --git a/tests/utils/create-e2e-fixture.ts b/tests/utils/create-e2e-fixture.ts
index 9718e28711..56e871f389 100644
--- a/tests/utils/create-e2e-fixture.ts
+++ b/tests/utils/create-e2e-fixture.ts
@@ -24,6 +24,16 @@ export interface DeployResult {
 
 type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'berry'
 
+const defaultValidateDeployOutput = async (siteAbsDir: string) => {
+  // by default we expect Frameworks API to be used in the build
+  const serverHandlerDir = join(siteAbsDir, '.netlify/functions/___netlify-server-handler')
+  if (!existsSync(serverHandlerDir)) {
+    throw new Error(`Server handler not found at ${siteAbsDir}`)
+  }
+}
+
+const staticExportValidateDeployOutput = defaultValidateDeployOutput //() => {}
+
 interface E2EConfig {
   packageManger?: PackageManager
   packagePath?: string
@@ -44,6 +54,10 @@ interface E2EConfig {
    * Site ID to deploy to. Defaults to the `NETLIFY_SITE_ID` environment variable or a default site.
    */
   siteId?: string
+  /**
+   *
+   */
+  validateDeployFiles?: typeof defaultValidateDeployOutput
 }
 
 /**
@@ -84,6 +98,14 @@ export const createE2EFixture = async (fixture: string, config: E2EConfig = {})
 
     const result = await deploySite(isolatedFixtureRoot, config)
 
+    {
+      const validateOutput = config.validateDeployFiles ?? defaultValidateDeployOutput
+
+      const siteRelDir = config.cwd ?? config.packagePath ?? ''
+
+      await validateOutput(join(isolatedFixtureRoot, siteRelDir))
+    }
+
     console.log(`🌍 Deployed site is live: ${result.url}`)
     deployID = result.deployID
     logs = result.logs
@@ -307,14 +329,17 @@ async function cleanup(dest: string, deployId?: string): Promise<void> {
 
 export const fixtureFactories = {
   simple: () => createE2EFixture('simple'),
-  outputExport: () => createE2EFixture('output-export'),
+  outputExport: () =>
+    createE2EFixture('output-export', { validateDeployFiles: staticExportValidateDeployOutput }),
   ouputExportPublishOut: () =>
     createE2EFixture('output-export', {
       publishDirectory: 'out',
+      validateDeployFiles: staticExportValidateDeployOutput,
     }),
   outputExportCustomDist: () =>
     createE2EFixture('output-export-custom-dist', {
       publishDirectory: 'custom-dist',
+      validateDeployFiles: staticExportValidateDeployOutput,
     }),
   distDir: () =>
     createE2EFixture('dist-dir', {
@@ -358,6 +383,10 @@ export const fixtureFactories = {
     createE2EFixture('cli-before-regional-blobs-support', {
       expectedCliVersion: '17.21.1',
     }),
+  cliBeforeFrameworksAPISupport: () =>
+    createE2EFixture('cli-before-frameworks-api-support', {
+      expectedCliVersion: '17.32.0',
+    }),
   yarnMonorepoWithPnpmLinker: () =>
     createE2EFixture('yarn-monorepo-with-pnpm-linker', {
       packageManger: 'berry',
diff --git a/tests/utils/fixture.ts b/tests/utils/fixture.ts
index 18ebb328fd..24ed56156b 100644
--- a/tests/utils/fixture.ts
+++ b/tests/utils/fixture.ts
@@ -198,7 +198,7 @@ export async function runPluginStep(
       // EDGE_FUNCTIONS_DIST: '.netlify/edge-functions-dist/',
       // CACHE_DIR: '.netlify/cache',
       // IS_LOCAL: true,
-      // NETLIFY_BUILD_VERSION: '29.23.4',
+      NETLIFY_BUILD_VERSION: '29.50.5',
       // INTERNAL_FUNCTIONS_SRC: '.netlify/functions-internal',
       // INTERNAL_EDGE_FUNCTIONS_SRC: '.netlify/edge-functions',
     },
@@ -312,28 +312,54 @@ export async function runPlugin(
     )
   }
 
-  await Promise.all([bundleEdgeFunctions(), bundleFunctions(), uploadBlobs(ctx, base.blobDir)])
+  await Promise.all([bundleEdgeFunctions(), bundleFunctions(), uploadBlobs(ctx, base)])
 
   return options
 }
 
-export async function uploadBlobs(ctx: FixtureTestContext, blobsDir: string) {
-  const files = await glob('**/*', {
-    dot: true,
-    cwd: blobsDir,
-  })
+export async function uploadBlobs(ctx: FixtureTestContext, pluginContext: PluginContext) {
+  if (pluginContext.blobsStrategy === 'frameworks-api') {
+    const files = await glob('**/blob', {
+      dot: true,
+      cwd: pluginContext.blobDir,
+    })
 
-  const keys = files.filter((file) => !basename(file).startsWith('$'))
-  await Promise.all(
-    keys.map(async (key) => {
-      const { dir, base } = parse(key)
-      const metaFile = join(blobsDir, dir, `$${base}.json`)
-      const metadata = await readFile(metaFile, 'utf-8')
-        .then((meta) => JSON.parse(meta))
-        .catch(() => ({}))
-      await ctx.blobStore.set(key, await readFile(join(blobsDir, key), 'utf-8'), { metadata })
-    }),
-  )
+    await Promise.all(
+      files.map(async (blobFilePath) => {
+        const { dir: key } = parse(blobFilePath)
+        const metaFile = join(pluginContext.blobDir, key, `blob.meta.json`)
+        const metadata = await readFile(metaFile, 'utf-8')
+          .then((meta) => JSON.parse(meta))
+          .catch(() => ({}))
+        await ctx.blobStore.set(
+          key,
+          await readFile(join(pluginContext.blobDir, blobFilePath), 'utf-8'),
+          {
+            metadata,
+          },
+        )
+      }),
+    )
+  } else {
+    const files = await glob('**/*', {
+      dot: true,
+      cwd: pluginContext.blobDir,
+    })
+
+    const keys = files.filter((file) => !basename(file).startsWith('$'))
+    await Promise.all(
+      keys.map(async (key) => {
+        const { dir, base } = parse(key)
+        const metaFile = join(pluginContext.blobDir, dir, `$${base}.json`)
+        const metadata = await readFile(metaFile, 'utf-8')
+          .then((meta) => JSON.parse(meta))
+          .catch(() => ({}))
+        await ctx.blobStore.set(key, await readFile(join(pluginContext.blobDir, key), 'utf-8'), {
+          metadata,
+        })
+      }),
+    )
+  }
 }
 
 const DEFAULT_FLAGS = {}