diff --git a/packages/build-info/src/frameworks/framework.ts b/packages/build-info/src/frameworks/framework.ts
index c31311b900..82567b31ed 100644
--- a/packages/build-info/src/frameworks/framework.ts
+++ b/packages/build-info/src/frameworks/framework.ts
@@ -45,8 +45,17 @@ export interface Framework {
   id: string
   name: string
   category: Category
+  /**
+   * If this is set, at least ONE of these must exist, anywhere in the project
+   */
   configFiles: string[]
+  /**
+   * If this is set, at least ONE of these must be present in the `package.json` `dependencies|devDependencies`
+   */
   npmDependencies: string[]
+  /**
+   * if this is set, NONE of these must be present in the `package.json` `dependencies|devDependencies`
+   */
   excludedNpmDependencies?: string[]
   version?: SemVer
   /** Information on how it was detected and how accurate the detection is */
@@ -255,9 +264,9 @@ export abstract class BaseFramework implements Framework {
   }
 
   /** detect if the framework config file is located somewhere up the tree */
-  private async detectConfigFile(): Promise<Detection | undefined> {
-    if (this.configFiles?.length) {
-      const config = await this.project.fs.findUp(this.configFiles, {
+  protected async detectConfigFile(configFiles: string[]): Promise<Detection | undefined> {
+    if (configFiles.length) {
+      const config = await this.project.fs.findUp(configFiles, {
         cwd: this.path || this.project.baseDirectory,
         stopAt: this.project.root,
       })
@@ -275,7 +284,7 @@ export abstract class BaseFramework implements Framework {
 
   /**
    * Checks if the project is using a specific framework:
-   * - if `npmDependencies` is set, one of them must be present in then `package.json` `dependencies|devDependencies`
+   * - if `npmDependencies` is set, one of them must be present in the `package.json` `dependencies|devDependencies`
    * - if `excludedNpmDependencies` is set, none of them must be present in the `package.json` `dependencies|devDependencies`
    * - if `configFiles` is set, one of the files must exist
    */
@@ -286,7 +295,7 @@ export abstract class BaseFramework implements Framework {
     }
 
     const npm = await this.detectNpmDependency()
-    const config = await this.detectConfigFile()
+    const config = await this.detectConfigFile(this.configFiles ?? [])
     this.detected = mergeDetections([npm, config])
 
     if (this.detected) {
diff --git a/packages/build-info/src/frameworks/hydrogen.test.ts b/packages/build-info/src/frameworks/hydrogen.test.ts
new file mode 100644
index 0000000000..fdf848f010
--- /dev/null
+++ b/packages/build-info/src/frameworks/hydrogen.test.ts
@@ -0,0 +1,86 @@
+import { beforeEach, expect, test } from 'vitest'
+
+import { mockFileSystem } from '../../tests/mock-file-system.js'
+import { NodeFS } from '../node/file-system.js'
+import { Project } from '../project.js'
+
+beforeEach((ctx) => {
+  ctx.fs = new NodeFS()
+})
+
+test('detects a Hydrogen v2 site using the Remix Classic Compiler', async ({ fs }) => {
+  const cwd = mockFileSystem({
+    'remix.config.js': '',
+    'package.json': JSON.stringify({
+      scripts: {
+        build: 'remix build',
+        dev: 'remix dev --manual -c "netlify dev"',
+        preview: 'netlify serve',
+      },
+      dependencies: {
+        '@netlify/edge-functions': '^2.2.0',
+        '@netlify/remix-edge-adapter': '^3.1.0',
+        '@netlify/remix-runtime': '^2.1.0',
+        '@remix-run/react': '^2.2.0',
+        '@shopify/cli': '3.50.0',
+        '@shopify/cli-hydrogen': '^6.0.0',
+        '@shopify/hydrogen': '^2023.10.0',
+        react: '^18.2.0',
+        'react-dom': '^18.2.0',
+      },
+      devDependencies: {
+        '@remix-run/dev': '^2.2.0',
+      },
+    }),
+  })
+  const detected = await new Project(fs, cwd).detectFrameworks()
+
+  const detectedFrameworks = (detected ?? []).map((framework) => framework.id)
+  expect(detectedFrameworks).not.toContain('remix')
+  expect(detectedFrameworks).not.toContain('vite')
+
+  expect(detected?.[0]?.id).toBe('hydrogen')
+  expect(detected?.[0]?.build?.command).toBe('remix build')
+  expect(detected?.[0]?.build?.directory).toBe('public')
+  expect(detected?.[0]?.dev?.command).toBe('remix dev --manual -c "netlify dev"')
+  expect(detected?.[0]?.dev?.port).toBeUndefined()
+})
+
+test('detects a Hydrogen v2 site using Remix Vite', async ({ fs }) => {
+  const cwd = mockFileSystem({
+    'vite.config.ts': '',
+    'package.json': JSON.stringify({
+      scripts: {
+        build: 'remix vite:build',
+        dev: 'shopify hydrogen dev --codegen',
+        preview: 'netlify serve',
+      },
+      dependencies: {
+        '@netlify/edge-functions': '^2.10.0',
+        '@netlify/remix-edge-adapter': '^3.4.0',
+        '@netlify/remix-runtime': '^2.3.0',
+        '@remix-run/react': '^2.11.2',
+        '@shopify/hydrogen': '^2024.7.4',
+        react: '^18.2.0',
+        'react-dom': '^18.2.0',
+      },
+      devDependencies: {
+        '@remix-run/dev': '^2.11.2',
+        '@shopify/cli': '^3.66.1',
+        '@shopify/hydrogen-codegen': '^0.3.1',
+        vite: '^5.4.3',
+      },
+    }),
+  })
+  const detected = await new Project(fs, cwd).detectFrameworks()
+
+  const detectedFrameworks = (detected ?? []).map((framework) => framework.id)
+  expect(detectedFrameworks).not.toContain('remix')
+  expect(detectedFrameworks).not.toContain('vite')
+
+  expect(detected?.[0]?.id).toBe('hydrogen')
+  expect(detected?.[0]?.build?.command).toBe('remix vite:build')
+  expect(detected?.[0]?.build?.directory).toBe('dist/client')
+  expect(detected?.[0]?.dev?.command).toBe('shopify hydrogen dev')
+  expect(detected?.[0]?.dev?.port).toBe(5173)
+})
diff --git a/packages/build-info/src/frameworks/hydrogen.ts b/packages/build-info/src/frameworks/hydrogen.ts
index d4135a53d0..24e31a550b 100644
--- a/packages/build-info/src/frameworks/hydrogen.ts
+++ b/packages/build-info/src/frameworks/hydrogen.ts
@@ -1,4 +1,32 @@
-import { BaseFramework, Category, Framework } from './framework.js'
+import { BaseFramework, Category, DetectedFramework, Framework } from './framework.js'
+
+const CLASSIC_COMPILER_CONFIG_FILES = ['remix.config.js']
+const CLASSIC_COMPILER_DEV = {
+  command: 'remix dev --manual -c "netlify dev"',
+}
+const CLASSIC_COMPILER_BUILD = {
+  command: 'remix build',
+  directory: 'public',
+}
+
+const VITE_CONFIG_FILES = [
+  'vite.config.js',
+  'vite.config.mjs',
+  'vite.config.cjs',
+  'vite.config.ts',
+  'vite.config.mts',
+  'vite.config.cts',
+]
+const VITE_DEV = {
+  command: 'shopify hydrogen dev',
+  port: 5173,
+}
+const VITE_BUILD = {
+  // This should be `shopify hydrogen build` but we use this as a workaround for
+  // https://github.com/Shopify/hydrogen/issues/2496 and https://github.com/Shopify/hydrogen/issues/2497.
+  command: 'remix vite:build',
+  directory: 'dist/client',
+}
 
 export class Hydrogen extends BaseFramework implements Framework {
   readonly id = 'hydrogen'
@@ -6,20 +34,32 @@ export class Hydrogen extends BaseFramework implements Framework {
   npmDependencies = ['@shopify/hydrogen']
   category = Category.SSG
 
-  dev = {
-    command: 'vite',
-    port: 3000,
-    pollingStrategies: [{ name: 'TCP' }],
-  }
-
-  build = {
-    command: 'npm run build',
-    directory: 'dist/client',
-  }
-
   logo = {
     default: '/logos/hydrogen/default.svg',
     light: '/logos/hydrogen/default.svg',
     dark: '/logos/hydrogen/default.svg',
   }
+
+  async detect(): Promise<DetectedFramework | undefined> {
+    await super.detect()
+
+    if (this.detected) {
+      const viteDetection = await this.detectConfigFile(VITE_CONFIG_FILES)
+      if (viteDetection) {
+        this.detected = viteDetection
+        this.dev = VITE_DEV
+        this.build = VITE_BUILD
+        return this as DetectedFramework
+      }
+      const classicCompilerDetection = await this.detectConfigFile(CLASSIC_COMPILER_CONFIG_FILES)
+      if (classicCompilerDetection) {
+        this.detected = classicCompilerDetection
+        this.dev = CLASSIC_COMPILER_DEV
+        this.build = CLASSIC_COMPILER_BUILD
+        return this as DetectedFramework
+      }
+      // If neither config file exists, it can't be a valid Hydrogen site for Netlify anyway.
+      return
+    }
+  }
 }
diff --git a/packages/build-info/src/frameworks/remix.test.ts b/packages/build-info/src/frameworks/remix.test.ts
new file mode 100644
index 0000000000..4ee96c15bf
--- /dev/null
+++ b/packages/build-info/src/frameworks/remix.test.ts
@@ -0,0 +1,283 @@
+import { beforeEach, expect, test } from 'vitest'
+
+import { mockFileSystem } from '../../tests/mock-file-system.js'
+import { NodeFS } from '../node/file-system.js'
+import { Project } from '../project.js'
+
+beforeEach((ctx) => {
+  ctx.fs = new NodeFS()
+})
+;[
+  [
+    'with origin SSR',
+    {
+      'vite.config.js': '',
+      'package.json': JSON.stringify({
+        scripts: {
+          build: 'remix vite:build',
+          dev: 'remix vite:dev',
+          start: 'remix-serve ./build/server/index.js',
+        },
+        dependencies: {
+          '@remix-run/node': '^2.9.2',
+          '@remix-run/react': '^2.9.2',
+          '@remix-run/serve': '^2.9.2',
+          react: '^18.2.0',
+          'react-dom': '^18.2.0',
+        },
+        devDependencies: {
+          '@netlify/remix-adapter': '^2.4.0',
+          '@remix-run/dev': '^2.9.2',
+          '@types/react': '^18.2.20',
+          '@types/react-dom': '^18.2.7',
+          vite: '^5.0.0',
+          'vite-tsconfig-paths': '^4.2.1',
+        },
+      }),
+    },
+  ] as const,
+  [
+    'with edge SSR',
+    {
+      'vite.config.js': '',
+      'package.json': JSON.stringify({
+        scripts: {
+          build: 'remix vite:build',
+          dev: 'remix vite:dev',
+          start: 'remix-serve ./build/server/index.js',
+        },
+        dependencies: {
+          '@netlify/remix-runtime': '^2.3.0',
+          '@remix-run/node': '^2.9.2',
+          '@remix-run/react': '^2.9.2',
+          '@remix-run/serve': '^2.9.2',
+          react: '^18.2.0',
+          'react-dom': '^18.2.0',
+        },
+        devDependencies: {
+          '@netlify/remix-edge-adapter': '^3.3.0',
+          '@remix-run/dev': '^2.9.2',
+          '@types/react': '^18.2.20',
+          '@types/react-dom': '^18.2.7',
+          vite: '^5.0.0',
+          'vite-tsconfig-paths': '^4.2.1',
+        },
+      }),
+    },
+  ] as const,
+].forEach(([description, files]) =>
+  test(`detects a Remix Vite site ${description}`, async ({ fs }) => {
+    const cwd = mockFileSystem(files)
+    const detected = await new Project(fs, cwd).detectFrameworks()
+
+    const detectedFrameworks = (detected ?? []).map((framework) => framework.id)
+    expect(detectedFrameworks).not.toContain('vite')
+
+    expect(detected?.[0]?.id).toBe('remix')
+    expect(detected?.[0]?.build?.command).toBe('remix vite:build')
+    expect(detected?.[0]?.build?.directory).toBe('build/client')
+    expect(detected?.[0]?.dev?.command).toBe('remix vite:dev')
+    expect(detected?.[0]?.dev?.port).toBe(5173)
+  }),
+)
+;[
+  [
+    'with origin SSR',
+    {
+      'remix.config.js': '',
+      'package.json': JSON.stringify({
+        scripts: {
+          build: 'remix build',
+          dev: 'remix dev',
+          start: 'netlify serve',
+          typecheck: 'tsc',
+        },
+        dependencies: {
+          '@netlify/functions': '^2.8.1',
+          '@remix-run/css-bundle': '^2.9.2',
+          '@remix-run/node': '^2.9.2',
+          '@remix-run/react': '^2.9.2',
+          react: '^18.2.0',
+          'react-dom': '^18.2.0',
+        },
+        devDependencies: {
+          '@netlify/remix-adapter': '^2.4.0',
+          '@remix-run/dev': '^2.9.2',
+          '@remix-run/serve': '^2.9.2',
+        },
+      }),
+    },
+  ] as const,
+  [
+    'with edge SSR',
+    {
+      'remix.config.js': '',
+      'package.json': JSON.stringify({
+        scripts: {
+          build: 'remix build',
+          dev: 'remix dev',
+          start: 'netlify serve',
+          typecheck: 'tsc',
+        },
+        dependencies: {
+          '@netlify/functions': '^2.8.1',
+          '@netlify/remix-runtime': '^2.3.0',
+          '@remix-run/css-bundle': '^2.9.2',
+          '@remix-run/node': '^2.9.2',
+          '@remix-run/react': '^2.9.2',
+          react: '^18.2.0',
+          'react-dom': '^18.2.0',
+        },
+        devDependencies: {
+          '@netlify/remix-edge-adapter': '^3.3.0',
+          '@remix-run/dev': '^2.9.2',
+          '@remix-run/serve': '^2.9.2',
+        },
+      }),
+    },
+  ] as const,
+  [
+    'with origin SSR via our legacy packages',
+    {
+      'remix.config.js': '',
+      'package.json': JSON.stringify({
+        scripts: {
+          build: 'remix build',
+          dev: 'remix dev',
+          start: 'netlify serve',
+          typecheck: 'tsc',
+        },
+        dependencies: {
+          '@remix-run/netlify': '1.7.4',
+          '@remix-run/node': '1.7.4',
+          '@remix-run/react': '1.7.4',
+          react: '18.2.0',
+          'react-dom': '18.2.0',
+        },
+        devDependencies: {
+          '@netlify/functions': '^1.0.0',
+          '@remix-run/dev': '1.7.4',
+          '@remix-run/serve': '1.7.4',
+        },
+      }),
+    },
+  ] as const,
+  [
+    'with edge SSR via our legacy packages',
+    {
+      'remix.config.js': '',
+      'package.json': JSON.stringify({
+        scripts: {
+          build: 'remix build',
+          dev: 'remix dev',
+          start: 'netlify serve',
+          typecheck: 'tsc',
+        },
+        dependencies: {
+          '@remix-run/netlify-edge': '1.7.4',
+          '@remix-run/node': '1.7.4',
+          '@remix-run/react': '1.7.4',
+          react: '18.2.0',
+          'react-dom': '18.2.0',
+        },
+        devDependencies: {
+          '@netlify/functions': '^1.0.0',
+          '@remix-run/dev': '1.7.4',
+          '@remix-run/serve': '1.7.4',
+        },
+      }),
+    },
+  ] as const,
+  [
+    'with origin SSR and the legacy remix package',
+    {
+      'remix.config.js': '',
+      'package.json': JSON.stringify({
+        scripts: {
+          build: 'remix build',
+          dev: 'remix dev',
+          start: 'netlify serve',
+          typecheck: 'tsc',
+        },
+        dependencies: {
+          '@remix-run/netlify': '1.7.4',
+          remix: '1.7.4',
+          react: '18.2.0',
+          'react-dom': '18.2.0',
+        },
+        devDependencies: {
+          '@netlify/functions': '^1.0.0',
+        },
+      }),
+    },
+  ] as const,
+  [
+    'with edge SSR and the legacy remix package',
+    {
+      'remix.config.js': '',
+      'package.json': JSON.stringify({
+        scripts: {
+          build: 'remix build',
+          dev: 'remix dev',
+          start: 'netlify serve',
+          typecheck: 'tsc',
+        },
+        dependencies: {
+          '@remix-run/netlify-edge': '1.7.4',
+          remix: '1.7.4',
+          react: '18.2.0',
+          'react-dom': '18.2.0',
+        },
+        devDependencies: {
+          '@netlify/functions': '^1.0.0',
+        },
+      }),
+    },
+  ] as const,
+].forEach(([description, files]) =>
+  test(`detects a Remix Classic Compiler site ${description}`, async ({ fs }) => {
+    const cwd = mockFileSystem(files)
+    const detected = await new Project(fs, cwd).detectFrameworks()
+
+    const detectedFrameworks = (detected ?? []).map((framework) => framework.id)
+    expect(detectedFrameworks).not.toContain('vite')
+
+    expect(detected?.[0]?.id).toBe('remix')
+    expect(detected?.[0]?.build?.command).toBe('remix build')
+    expect(detected?.[0]?.build?.directory).toBe('public')
+    expect(detected?.[0]?.dev?.command).toBe('remix watch')
+    expect(detected?.[0]?.dev?.port).toBeUndefined()
+  }),
+)
+
+test('does not detect an invalid Remix site with no config file', async ({ fs }) => {
+  const cwd = mockFileSystem({
+    'package.json': JSON.stringify({
+      scripts: {
+        build: 'remix vite:build',
+        dev: 'remix vite:dev',
+        start: 'remix-serve ./build/server/index.js',
+      },
+      dependencies: {
+        '@netlify/remix-runtime': '^2.3.0',
+        '@remix-run/node': '^2.9.2',
+        '@remix-run/react': '^2.9.2',
+        '@remix-run/serve': '^2.9.2',
+        react: '^18.2.0',
+        'react-dom': '^18.2.0',
+      },
+      devDependencies: {
+        '@netlify/remix-edge-adapter': '^3.3.0',
+        '@remix-run/dev': '^2.9.2',
+        '@types/react': '^18.2.20',
+        '@types/react-dom': '^18.2.7',
+        vite: '^5.0.0',
+        'vite-tsconfig-paths': '^4.2.1',
+      },
+    }),
+  })
+  const detected = await new Project(fs, cwd).detectFrameworks()
+
+  const detectedFrameworks = (detected ?? []).map((framework) => framework.id)
+  expect(detectedFrameworks).not.toContain('remix')
+})
diff --git a/packages/build-info/src/frameworks/remix.ts b/packages/build-info/src/frameworks/remix.ts
index 9ad2ceeb6a..0cdcea9157 100644
--- a/packages/build-info/src/frameworks/remix.ts
+++ b/packages/build-info/src/frameworks/remix.ts
@@ -1,24 +1,76 @@
-import { BaseFramework, Category, Framework } from './framework.js'
+import { BaseFramework, Category, DetectedFramework, Framework } from './framework.js'
+
+const CLASSIC_COMPILER_CONFIG_FILES = ['remix.config.js']
+const CLASSIC_COMPILER_DEV = {
+  command: 'remix watch',
+}
+const CLASSIC_COMPILER_BUILD = {
+  command: 'remix build',
+  directory: 'public',
+}
+
+const VITE_CONFIG_FILES = [
+  'vite.config.js',
+  'vite.config.mjs',
+  'vite.config.cjs',
+  'vite.config.ts',
+  'vite.config.mts',
+  'vite.config.cts',
+]
+const VITE_DEV = {
+  command: 'remix vite:dev',
+  port: 5173,
+}
+const VITE_BUILD = {
+  command: 'remix vite:build',
+  directory: 'build/client',
+}
 
 export class Remix extends BaseFramework implements Framework {
   readonly id = 'remix'
   name = 'Remix'
-  npmDependencies = ['remix', '@remix-run/netlify', '@remix-run/netlify-edge']
-  configFiles = ['remix.config.js']
+  npmDependencies = [
+    '@remix-run/react',
+    '@remix-run/dev',
+    '@remix-run/server-runtime',
+    '@netlify/remix-adapter',
+    '@netlify/remix-edge-adapter',
+    '@netlify/remix-runtime',
+    // Deprecated package name (deprecated in 1.6, removed in 2.0)
+    'remix',
+    // Deprecated Netlify packages
+    '@remix-run/netlify',
+    '@remix-run/netlify-edge',
+  ]
+  excludedNpmDependencies = ['@shopify/hydrogen']
   category = Category.SSG
 
-  dev = {
-    command: 'remix watch',
-  }
-
-  build = {
-    command: 'remix build',
-    directory: 'public',
-  }
-
   logo = {
     default: '/logos/remix/default.svg',
     light: '/logos/remix/light.svg',
     dark: '/logos/remix/dark.svg',
   }
+
+  async detect(): Promise<DetectedFramework | undefined> {
+    await super.detect()
+
+    if (this.detected) {
+      const viteDetection = await this.detectConfigFile(VITE_CONFIG_FILES)
+      if (viteDetection) {
+        this.detected = viteDetection
+        this.dev = VITE_DEV
+        this.build = VITE_BUILD
+        return this as DetectedFramework
+      }
+      const classicCompilerDetection = await this.detectConfigFile(CLASSIC_COMPILER_CONFIG_FILES)
+      if (classicCompilerDetection) {
+        this.detected = classicCompilerDetection
+        this.dev = CLASSIC_COMPILER_DEV
+        this.build = CLASSIC_COMPILER_BUILD
+        return this as DetectedFramework
+      }
+      // If neither config file exists, it can't be a valid Remix site for Netlify anyway.
+      return
+    }
+  }
 }
diff --git a/packages/build-info/src/frameworks/vite.ts b/packages/build-info/src/frameworks/vite.ts
index 2ee26ef80f..52f2a258c1 100644
--- a/packages/build-info/src/frameworks/vite.ts
+++ b/packages/build-info/src/frameworks/vite.ts
@@ -5,6 +5,9 @@ export class Vite extends BaseFramework implements Framework {
   name = 'Vite'
   npmDependencies = ['vite']
   excludedNpmDependencies = [
+    '@remix-run/react',
+    '@remix-run/dev',
+    '@remix-run/server-runtime',
     '@shopify/hydrogen',
     '@builder.io/qwik',
     'solid-start',