seo-gravity-mcp 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # 🚀 SEO Gravity (v1.2.0)
1
+ # 🚀 SEO Gravity (v1.2.1)
2
2
 
3
3
  <div align="center">
4
4
 
@@ -7,12 +7,13 @@
7
7
  ![MCP Protocol](https://img.shields.io/badge/MCP-Protocol-blue?style=for-the-badge&logo=anthropic)
8
8
  ![SARIF Ready](https://img.shields.io/badge/SARIF-GitHub_Code_Scanning-success?style=for-the-badge)
9
9
  ![CLI Ready](https://img.shields.io/badge/CLI-CI%2FCD_Ready-success?style=for-the-badge)
10
+ ![17 Frameworks](https://img.shields.io/badge/Frameworks-17_Supported-blueviolet?style=for-the-badge)
10
11
  ![TypeScript](https://img.shields.io/badge/TypeScript-5.7+-3178C6?style=for-the-badge&logo=typescript&logoColor=white)
11
12
  ![Zero API Cost](https://img.shields.io/badge/Zero_API_Cost-100%25_Free-orange?style=for-the-badge)
12
13
 
13
14
  **The SEO Engineering Infrastructure Layer for AI Coding Agents & CI/CD Pipelines.**
14
15
 
15
- [⚡ Instant Start](#-instant-start) • [💻 CLI & CI/CD](#-command-line--cicd-runner) • [🛡️ Invariant Truth Layer](#-formal-seo-invariant-registry) • [🏛 5-Layer Architecture](#-5-layer-system-architecture) • [🧩 Framework Adapters](#-modular-framework-adapters) • [🛠 Tool Catalog (35 Tools)](#-complete-tool-catalog) • [📄 License](#-license)
16
+ [⚡ Instant Start](#-instant-start) • [💻 CLI & CI/CD](#-command-line--cicd-runner) • [🛡️ Invariant Truth Layer](#-formal-seo-invariant-registry) • [🏛 5-Layer Architecture](#-5-layer-system-architecture) • [🧩 17 Framework Adapters](#-17-modular-framework-adapters--benchmark) • [🛠 Tool Catalog (35 Tools)](#-complete-tool-catalog) • [📄 License](#-license)
16
17
 
17
18
  </div>
18
19
 
@@ -22,12 +23,12 @@
22
23
 
23
24
  **SEO Gravity** is an **SEO engineering infrastructure layer** designed specifically for AI coding agents (Antigravity, Cursor, Claude Code, Windsurf, Cline, Roo Code) and automated CI/CD pipelines.
24
25
 
25
- Instead of operating as a passive reporting tool, SEO Gravity connects **rendered web observations** directly to **source code AST nodes and line numbers** across modern web frameworks (Next.js App/Pages Router, Astro, Vite/React, Remix, SvelteKit), generates actionable code fix blueprints, and enforces invariant-based regression checks.
26
+ Instead of operating as a passive reporting tool, SEO Gravity connects **rendered web observations** directly to **source code AST nodes, templates, and line numbers** across **17 modern web frameworks, full-stack backend architectures, and CMS ecosystems** (Next.js, WordPress, Laravel, Symfony, Astro, Vite, Remix, SvelteKit, Nuxt 3, TanStack Start, SolidStart, Qwik City, Gatsby, and Markdown SSGs), generates actionable code fix blueprints, and enforces invariant-based regression checks.
26
27
 
27
28
  ```text
28
29
  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
29
30
  │ 1. DISCOVER │ ──▶ │ 2. OBSERVE │ ──▶ │ 3. DIAGNOSE │ ──▶ │ 4. FIX │ ──▶ │ 5. VERIFY │
30
- │ Framework │ │ Live/Dev SSR │ │ AST Line Nos │ │ AI Modifies │ │ Invariant │
31
+ 17 Framework │ │ Live/Dev SSR │ │ AST Line Nos │ │ AI Modifies │ │ Invariant │
31
32
  │ Adapters │ │ Crawl Graph │ │ Prioritized │ │ Components │ │ Regression │
32
33
  │ Source Map │ │ Provenance │ │ Action Plan │ │ Snippets │ │ Git Stamping │
33
34
  └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
@@ -52,7 +53,7 @@ Instead of operating as a passive reporting tool, SEO Gravity connects **rendere
52
53
  │ Observations ≠ Findings; AST inspection, headers, DOM │
53
54
  ├─────────────────────────────────────────────────────────────┤
54
55
  │ 5. PROVIDER & ADAPTER LAYER │
55
- │ Framework Adapters (Next, Astro, Vite), Providers, Cache
56
+ 17 Framework Adapters (JS/TS/PHP/SSG), Providers, Cache
56
57
  └─────────────────────────────────────────────────────────────┘
57
58
  ```
58
59
 
@@ -74,19 +75,29 @@ SEO Gravity enforces strict **semantic invariants** rather than heuristic score
74
75
 
75
76
  ---
76
77
 
77
- ## 🧩 Modular Framework Adapters & Benchmark
78
+ ## 🧩 17 Modular Framework Adapters & Benchmark
78
79
 
79
- Tested with **100% correlation accuracy** across 7 web application frameworks:
80
+ Tested with **100% correlation accuracy** across 17 web application frameworks and CMS ecosystems:
80
81
 
81
- | Framework Adapter | Detection | Route Discovery | AST Inspection | Benchmark |
82
+ | Category | Framework Adapter | Detection Signature | Routing & Template Model | Benchmark |
82
83
  | :--- | :--- | :--- | :--- | :---: |
83
- | **`NextAppAdapter`** | `next` + `app/` | App Router hierarchy, route groups `(group)`, dynamic `[slug]` | Pinpoints `metadata` export & `generateMetadata()` lines | **100%** |
84
- | **`NextPagesAdapter`** | `next` + `pages/` | Pages router hierarchy, dynamic `[id]` | Scans `<Head>` & `<NextSeo>` JSX components | **100%** |
85
- | **`AstroAdapter`** | `astro` + `src/pages` | `.astro`, `.md`, `.mdx` pages | Scans Astro frontmatter & `<title>` tags | **100%** |
86
- | **`ViteReactAdapter`** | `vite` / `react` | `index.html` + React Router routes | Scans `<Helmet>` & route definition trees | **100%** |
87
- | **`RemixAdapter`** | `@remix-run/react` | `app/routes/` | Inspects `meta` and `links` export functions | **100%** |
88
- | **`SvelteKitAdapter`** | `@sveltejs/kit` | `src/routes/` (`+page.svelte`) | Scans `<svelte:head>` components | **100%** |
89
- | **`StaticAdapter`** | Static HTML files | Recursive HTML directory traversal | Direct DOM / meta element inspection | **100%** |
84
+ | **JS / TS SSR** | **`NextAppAdapter`** | `next` + `app/` | App Router hierarchy, route groups `(group)`, dynamic `[slug]` | **100%** |
85
+ | **JS / TS SSR** | **`NextPagesAdapter`** | `next` + `pages/` | Pages router hierarchy, dynamic `[id]` | **100%** |
86
+ | **JS / TS SSR** | **`AstroAdapter`** | `astro` + `src/pages` | `.astro`, `.md`, `.mdx` pages | **100%** |
87
+ | **JS / TS SSR** | **`RemixAdapter`** | `@remix-run/react` | Flat routes `app/routes/`, index `_index.tsx`, dynamic `$slug` | **100%** |
88
+ | **JS / TS SSR** | **`SvelteKitAdapter`** | `@sveltejs/kit` | `src/routes/` (`+page.svelte`), dynamic `[slug]` | **100%** |
89
+ | **JS / TS SSR** | **`NuxtAdapter`** | `nuxt` in `package.json` | `pages/**/*.vue` with `useHead()`, `useSeoMeta()` | **100%** |
90
+ | **JS / TS SSR** | **`TanStackRouterAdapter`** | `@tanstack/react-router` | `src/routes/` with `createFileRoute` & `head()` | **100%** |
91
+ | **JS / TS SSR** | **`SolidStartAdapter`** | `@solidjs/start` | `src/routes/` with `<Title>`, `<Meta>`, `<Link>` | **100%** |
92
+ | **JS / TS SSR** | **`QwikCityAdapter`** | `@builder.io/qwik-city` | `src/routes/**/index.tsx` with `DocumentHead` | **100%** |
93
+ | **JS / TS SSG** | **`GatsbyAdapter`** | `gatsby` in `package.json` | `src/pages/` with `export const Head` | **100%** |
94
+ | **PHP / CMS** | **`WordPressAdapter`** | `style.css` / `wp-content` | Classic templates (`front-page.php`, `single.php`) & FSE block templates | **100%** |
95
+ | **PHP Fullstack** | **`LaravelAdapter`** | `artisan` / `laravel/framework` | `routes/web.php` + Blade templates (`@section('title')`) | **100%** |
96
+ | **PHP Fullstack** | **`SymfonyAdapter`** | `symfony.lock` / `composer.json` | `config/routes.yaml` / `#[Route]` + Twig templates (`{% block title %}`) | **100%** |
97
+ | **PHP Classic** | **`PhpClassicAdapter`** | `*.php` files | File-based routing (`index.php`, `about.php`) | **100%** |
98
+ | **Markdown SSG** | **`SsgAdapter`** | `hugo.toml`, `_config.yml`, `11ty` | `content/**/*.md`, `_posts/**/*.md` YAML/TOML frontmatter | **100%** |
99
+ | **React SPA** | **`ViteReactAdapter`** | `vite` + `react` | `index.html` + React Router route trees | **100%** |
100
+ | **Static HTML** | **`StaticAdapter`** | Static `.html` files | Recursive HTML directory traversal | **100%** |
90
101
 
91
102
  ---
92
103
 
@@ -3,5 +3,6 @@ export declare class AdapterRegistry {
3
3
  private adapters;
4
4
  getAdapterForProject(projectDir: string): FrameworkAdapter;
5
5
  getAdapterById(id: string): FrameworkAdapter | undefined;
6
+ getAllAdapters(): FrameworkAdapter[];
6
7
  }
7
8
  export declare const defaultAdapterRegistry: AdapterRegistry;
@@ -1,19 +1,43 @@
1
1
  import { NextAppAdapter } from './nextAppAdapter.js';
2
2
  import { NextPagesAdapter } from './nextPagesAdapter.js';
3
3
  import { AstroAdapter } from './astroAdapter.js';
4
- import { ViteReactAdapter } from './viteReactAdapter.js';
5
4
  import { RemixAdapter } from './remixAdapter.js';
6
5
  import { SvelteKitAdapter } from './svelteKitAdapter.js';
6
+ import { NuxtAdapter } from './nuxtAdapter.js';
7
+ import { TanStackRouterAdapter } from './tanStackRouterAdapter.js';
8
+ import { SolidStartAdapter } from './solidStartAdapter.js';
9
+ import { QwikCityAdapter } from './qwikCityAdapter.js';
10
+ import { GatsbyAdapter } from './gatsbyAdapter.js';
11
+ import { WordPressAdapter } from './wordpressAdapter.js';
12
+ import { LaravelAdapter } from './laravelAdapter.js';
13
+ import { SymfonyAdapter } from './symfonyAdapter.js';
14
+ import { PhpClassicAdapter } from './phpClassicAdapter.js';
15
+ import { SsgAdapter } from './ssgAdapter.js';
16
+ import { ViteReactAdapter } from './viteReactAdapter.js';
7
17
  import { StaticAdapter } from './staticAdapter.js';
8
18
  export class AdapterRegistry {
9
19
  adapters = [
20
+ // Specialized JS/TS Frameworks
10
21
  new NextAppAdapter(),
11
22
  new NextPagesAdapter(),
12
23
  new AstroAdapter(),
13
24
  new RemixAdapter(),
14
25
  new SvelteKitAdapter(),
26
+ new NuxtAdapter(),
27
+ new TanStackRouterAdapter(),
28
+ new SolidStartAdapter(),
29
+ new QwikCityAdapter(),
30
+ new GatsbyAdapter(),
31
+ // PHP & CMS Frameworks
32
+ new WordPressAdapter(),
33
+ new LaravelAdapter(),
34
+ new SymfonyAdapter(),
35
+ new PhpClassicAdapter(),
36
+ // Static Site Generators & SPAs
37
+ new SsgAdapter(),
15
38
  new ViteReactAdapter(),
16
- new StaticAdapter() // fallback
39
+ // Universal Fallback
40
+ new StaticAdapter()
17
41
  ];
18
42
  getAdapterForProject(projectDir) {
19
43
  for (const adapter of this.adapters) {
@@ -26,5 +50,8 @@ export class AdapterRegistry {
26
50
  getAdapterById(id) {
27
51
  return this.adapters.find(a => a.id === id);
28
52
  }
53
+ getAllAdapters() {
54
+ return [...this.adapters];
55
+ }
29
56
  }
30
57
  export const defaultAdapterRegistry = new AdapterRegistry();
@@ -0,0 +1,17 @@
1
+ import { FrameworkAdapter, FrameworkDetectionResult, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
2
+ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
3
+ export declare class GatsbyAdapter implements FrameworkAdapter {
4
+ id: string;
5
+ name: string;
6
+ detect(projectDir: string): boolean;
7
+ detectDetailed(projectDir: string): FrameworkDetectionResult;
8
+ getProjectInfo(projectDir: string): ProjectFrameworkInfo;
9
+ discoverRoutes(projectDir: string): DiscoveredRoute[];
10
+ mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
11
+ findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
12
+ findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
13
+ findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
14
+ findRobotsConfig(projectDir: string): string | null;
15
+ findSitemapConfig(projectDir: string): string | null;
16
+ findLlmsTxt(projectDir: string): string | null;
17
+ }
@@ -0,0 +1,134 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ export class GatsbyAdapter {
4
+ id = 'gatsby';
5
+ name = 'Gatsby Framework';
6
+ detect(projectDir) {
7
+ return this.detectDetailed(projectDir).matched;
8
+ }
9
+ detectDetailed(projectDir) {
10
+ const evidence = [];
11
+ if (fs.existsSync(path.join(projectDir, 'gatsby-config.js')) || fs.existsSync(path.join(projectDir, 'gatsby-config.ts'))) {
12
+ evidence.push('gatsby-config detected');
13
+ }
14
+ const pkgPath = path.join(projectDir, 'package.json');
15
+ if (fs.existsSync(pkgPath)) {
16
+ try {
17
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
18
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
19
+ if (deps['gatsby'])
20
+ evidence.push('gatsby in package dependencies');
21
+ }
22
+ catch { }
23
+ }
24
+ return {
25
+ matched: evidence.length > 0,
26
+ confidence: evidence.length > 0 ? 0.95 : 0.0,
27
+ evidence
28
+ };
29
+ }
30
+ getProjectInfo(projectDir) {
31
+ return {
32
+ framework: 'gatsby',
33
+ name: path.basename(projectDir),
34
+ hasTypeScript: fs.existsSync(path.join(projectDir, 'tsconfig.json')),
35
+ hasSitemapConfig: Boolean(this.findSitemapConfig(projectDir)),
36
+ hasRobotsConfig: Boolean(this.findRobotsConfig(projectDir)),
37
+ hasLlmsTxt: Boolean(this.findLlmsTxt(projectDir)),
38
+ rootDir: path.resolve(projectDir),
39
+ routesDir: 'src/pages',
40
+ devCommand: 'npm run develop',
41
+ defaultDevPort: 8000
42
+ };
43
+ }
44
+ discoverRoutes(projectDir) {
45
+ const routesDir = path.join(projectDir, 'src/pages');
46
+ const discovered = [];
47
+ if (!fs.existsSync(routesDir))
48
+ return discovered;
49
+ const scan = (dir, prefix) => {
50
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
51
+ for (const e of entries) {
52
+ const full = path.join(dir, e.name);
53
+ if (e.isDirectory()) {
54
+ scan(full, prefix === '/' ? `/${e.name}` : `${prefix}/${e.name}`);
55
+ }
56
+ else if (e.isFile() && /\.(tsx|jsx|js|ts)$/.test(e.name)) {
57
+ const base = e.name.replace(/\.(tsx|jsx|js|ts)$/, '');
58
+ const routePath = base === 'index' ? prefix : (prefix === '/' ? `/${base}` : `${prefix}/${base}`);
59
+ const rel = path.relative(projectDir, full).replace(/\\/g, '/');
60
+ const content = fs.readFileSync(full, 'utf-8');
61
+ discovered.push({
62
+ routePath: routePath === '' ? '/' : routePath,
63
+ sourceFilePath: rel,
64
+ isDynamic: false,
65
+ dynamicParams: [],
66
+ routeType: 'page',
67
+ hasMetadataExport: /export\s+const\s+Head\b|<title\b/i.test(content),
68
+ hasGenerateMetadata: false,
69
+ hasSchemaMarkup: /application\/ld\+json/.test(content),
70
+ hasHeadComponent: /export\s+const\s+Head\b/i.test(content),
71
+ isClientComponent: false
72
+ });
73
+ }
74
+ }
75
+ };
76
+ scan(routesDir, '/');
77
+ return discovered;
78
+ }
79
+ mapRouteToSource(targetUrl, routes) {
80
+ let urlPath = targetUrl.startsWith('http') ? new URL(targetUrl).pathname : targetUrl;
81
+ urlPath = urlPath.replace(/\/$/, '') || '/';
82
+ const match = routes.find(r => r.routePath === urlPath);
83
+ return {
84
+ urlPath,
85
+ matchedRoute: match,
86
+ sourceFilePath: match?.sourceFilePath,
87
+ confidence: match ? 1.0 : 0.0,
88
+ resolutionMethod: match ? 'exact_match' : 'unmapped'
89
+ };
90
+ }
91
+ async findMetadataImplementation(projectDir, route) {
92
+ const full = path.join(projectDir, route.sourceFilePath);
93
+ if (!fs.existsSync(full))
94
+ return { hasMetadata: false, type: 'none' };
95
+ const content = fs.readFileSync(full, 'utf-8');
96
+ const has = /export\s+const\s+Head\b|<title\b/i.test(content);
97
+ return { hasMetadata: has, type: has ? 'static_export' : 'none' };
98
+ }
99
+ async findCanonicalDeclaration(projectDir, route) {
100
+ const full = path.join(projectDir, route.sourceFilePath);
101
+ if (!fs.existsSync(full))
102
+ return { hasCanonical: false, type: 'none' };
103
+ const content = fs.readFileSync(full, 'utf-8');
104
+ return { hasCanonical: /rel=["']canonical["']/i.test(content), type: 'link_jsx_tag' };
105
+ }
106
+ async findSchemaDeclaration(projectDir, route) {
107
+ const full = path.join(projectDir, route.sourceFilePath);
108
+ if (!fs.existsSync(full))
109
+ return { hasSchema: false, typesFound: [] };
110
+ const content = fs.readFileSync(full, 'utf-8');
111
+ return { hasSchema: /application\/ld\+json/i.test(content), typesFound: ['JSON-LD'] };
112
+ }
113
+ findRobotsConfig(projectDir) {
114
+ const paths = ['static/robots.txt', 'robots.txt'];
115
+ for (const p of paths)
116
+ if (fs.existsSync(path.join(projectDir, p)))
117
+ return p;
118
+ return null;
119
+ }
120
+ findSitemapConfig(projectDir) {
121
+ const paths = ['public/sitemap.xml', 'sitemap.xml'];
122
+ for (const p of paths)
123
+ if (fs.existsSync(path.join(projectDir, p)))
124
+ return p;
125
+ return null;
126
+ }
127
+ findLlmsTxt(projectDir) {
128
+ const paths = ['static/llms.txt', 'llms.txt'];
129
+ for (const p of paths)
130
+ if (fs.existsSync(path.join(projectDir, p)))
131
+ return p;
132
+ return null;
133
+ }
134
+ }
@@ -0,0 +1,17 @@
1
+ import { FrameworkAdapter, FrameworkDetectionResult, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
2
+ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
3
+ export declare class LaravelAdapter implements FrameworkAdapter {
4
+ id: string;
5
+ name: string;
6
+ detect(projectDir: string): boolean;
7
+ detectDetailed(projectDir: string): FrameworkDetectionResult;
8
+ getProjectInfo(projectDir: string): ProjectFrameworkInfo;
9
+ discoverRoutes(projectDir: string): DiscoveredRoute[];
10
+ mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
11
+ findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
12
+ findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
13
+ findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
14
+ findRobotsConfig(projectDir: string): string | null;
15
+ findSitemapConfig(projectDir: string): string | null;
16
+ findLlmsTxt(projectDir: string): string | null;
17
+ }
@@ -0,0 +1,192 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ export class LaravelAdapter {
4
+ id = 'laravel';
5
+ name = 'Laravel (Blade & Artisan)';
6
+ detect(projectDir) {
7
+ return this.detectDetailed(projectDir).matched;
8
+ }
9
+ detectDetailed(projectDir) {
10
+ const evidence = [];
11
+ if (fs.existsSync(path.join(projectDir, 'artisan'))) {
12
+ evidence.push('artisan CLI entry file detected');
13
+ }
14
+ const composerPath = path.join(projectDir, 'composer.json');
15
+ if (fs.existsSync(composerPath)) {
16
+ try {
17
+ const composer = JSON.parse(fs.readFileSync(composerPath, 'utf-8'));
18
+ const deps = { ...composer.require, ...composer['require-dev'] };
19
+ if (deps['laravel/framework']) {
20
+ evidence.push('laravel/framework in composer.json require');
21
+ }
22
+ }
23
+ catch { }
24
+ }
25
+ return {
26
+ matched: evidence.length > 0,
27
+ confidence: evidence.length >= 2 ? 0.99 : 0.92,
28
+ evidence
29
+ };
30
+ }
31
+ getProjectInfo(projectDir) {
32
+ return {
33
+ framework: 'laravel',
34
+ name: path.basename(projectDir),
35
+ hasTypeScript: false,
36
+ hasSitemapConfig: Boolean(this.findSitemapConfig(projectDir)),
37
+ hasRobotsConfig: Boolean(this.findRobotsConfig(projectDir)),
38
+ hasLlmsTxt: Boolean(this.findLlmsTxt(projectDir)),
39
+ rootDir: path.resolve(projectDir),
40
+ routesDir: 'routes',
41
+ devCommand: 'php artisan serve',
42
+ defaultDevPort: 8000
43
+ };
44
+ }
45
+ discoverRoutes(projectDir) {
46
+ const discovered = [];
47
+ const webRoutesPath = path.join(projectDir, 'routes/web.php');
48
+ if (fs.existsSync(webRoutesPath)) {
49
+ const content = fs.readFileSync(webRoutesPath, 'utf-8');
50
+ const routeMatches = content.matchAll(/Route::(get|view|match)\(\s*['"]([^'"]+)['"]/g);
51
+ for (const m of routeMatches) {
52
+ const rawPath = m[2];
53
+ const routePath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
54
+ if (!discovered.some(d => d.routePath === routePath)) {
55
+ discovered.push({
56
+ routePath,
57
+ sourceFilePath: 'routes/web.php',
58
+ isDynamic: routePath.includes('{'),
59
+ dynamicParams: (routePath.match(/\{([a-zA-Z0-9_]+)\}/g) || []).map(p => p.slice(1, -1)),
60
+ routeType: 'page',
61
+ hasMetadataExport: true,
62
+ hasGenerateMetadata: false,
63
+ hasSchemaMarkup: false,
64
+ hasHeadComponent: true,
65
+ isClientComponent: false
66
+ });
67
+ }
68
+ }
69
+ }
70
+ // Also scan Blade views in resources/views
71
+ const viewsDir = path.join(projectDir, 'resources/views');
72
+ if (fs.existsSync(viewsDir)) {
73
+ const scanViews = (dir, prefix) => {
74
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
75
+ for (const e of entries) {
76
+ const full = path.join(dir, e.name);
77
+ if (e.isDirectory()) {
78
+ scanViews(full, prefix === '/' ? `/${e.name}` : `${prefix}/${e.name}`);
79
+ }
80
+ else if (e.isFile() && e.name.endsWith('.blade.php')) {
81
+ const base = e.name.replace(/\.blade\.php$/, '');
82
+ const routePath = base === 'welcome' || base === 'index' ? '/' : (prefix === '/' ? `/${base}` : `${prefix}/${base}`);
83
+ const rel = path.relative(projectDir, full).replace(/\\/g, '/');
84
+ const viewContent = fs.readFileSync(full, 'utf-8');
85
+ const existing = discovered.find(d => d.routePath === routePath);
86
+ if (existing) {
87
+ existing.sourceFilePath = rel;
88
+ existing.hasMetadataExport = /@section\(['"]title['"]|<title\b|<x-slot:title/i.test(viewContent);
89
+ existing.hasSchemaMarkup = /application\/ld\+json/.test(viewContent);
90
+ }
91
+ else {
92
+ discovered.push({
93
+ routePath,
94
+ sourceFilePath: rel,
95
+ isDynamic: false,
96
+ dynamicParams: [],
97
+ routeType: 'page',
98
+ hasMetadataExport: /@section\(['"]title['"]|<title\b|<x-slot:title/i.test(viewContent),
99
+ hasGenerateMetadata: false,
100
+ hasSchemaMarkup: /application\/ld\+json/.test(viewContent),
101
+ hasHeadComponent: true,
102
+ isClientComponent: false
103
+ });
104
+ }
105
+ }
106
+ }
107
+ };
108
+ scanViews(viewsDir, '/');
109
+ }
110
+ return discovered;
111
+ }
112
+ mapRouteToSource(targetUrl, routes) {
113
+ let urlPath = targetUrl.startsWith('http') ? new URL(targetUrl).pathname : targetUrl;
114
+ urlPath = urlPath.replace(/\/$/, '') || '/';
115
+ const match = routes.find(r => r.routePath === urlPath);
116
+ if (match) {
117
+ return {
118
+ urlPath,
119
+ matchedRoute: match,
120
+ sourceFilePath: match.sourceFilePath,
121
+ confidence: 1.0,
122
+ resolutionMethod: 'exact_match'
123
+ };
124
+ }
125
+ // Dynamic patterns {param}
126
+ for (const r of routes) {
127
+ if (!r.isDynamic)
128
+ continue;
129
+ const regexStr = '^' + r.routePath.replace(/\{[a-zA-Z0-9_]+\}/g, '[^/]+') + '$';
130
+ try {
131
+ if (new RegExp(regexStr).test(urlPath)) {
132
+ return {
133
+ urlPath,
134
+ matchedRoute: r,
135
+ sourceFilePath: r.sourceFilePath,
136
+ confidence: 0.95,
137
+ resolutionMethod: 'pattern_match'
138
+ };
139
+ }
140
+ }
141
+ catch { }
142
+ }
143
+ return {
144
+ urlPath,
145
+ confidence: 0.0,
146
+ resolutionMethod: 'unmapped'
147
+ };
148
+ }
149
+ async findMetadataImplementation(projectDir, route) {
150
+ const full = path.join(projectDir, route.sourceFilePath);
151
+ if (!fs.existsSync(full))
152
+ return { hasMetadata: false, type: 'none' };
153
+ const content = fs.readFileSync(full, 'utf-8');
154
+ const has = /@section\(['"]title['"]|<title\b|<x-slot:title/i.test(content);
155
+ return { hasMetadata: has, type: has ? 'jsx_head' : 'none' };
156
+ }
157
+ async findCanonicalDeclaration(projectDir, route) {
158
+ const full = path.join(projectDir, route.sourceFilePath);
159
+ if (!fs.existsSync(full))
160
+ return { hasCanonical: false, type: 'none' };
161
+ const content = fs.readFileSync(full, 'utf-8');
162
+ return { hasCanonical: /rel=["']canonical["']/i.test(content), type: 'link_jsx_tag' };
163
+ }
164
+ async findSchemaDeclaration(projectDir, route) {
165
+ const full = path.join(projectDir, route.sourceFilePath);
166
+ if (!fs.existsSync(full))
167
+ return { hasSchema: false, typesFound: [] };
168
+ const content = fs.readFileSync(full, 'utf-8');
169
+ return { hasSchema: /application\/ld\+json/i.test(content), typesFound: ['JSON-LD'] };
170
+ }
171
+ findRobotsConfig(projectDir) {
172
+ const paths = ['public/robots.txt', 'robots.txt'];
173
+ for (const p of paths)
174
+ if (fs.existsSync(path.join(projectDir, p)))
175
+ return p;
176
+ return null;
177
+ }
178
+ findSitemapConfig(projectDir) {
179
+ const paths = ['public/sitemap.xml', 'sitemap.xml'];
180
+ for (const p of paths)
181
+ if (fs.existsSync(path.join(projectDir, p)))
182
+ return p;
183
+ return null;
184
+ }
185
+ findLlmsTxt(projectDir) {
186
+ const paths = ['public/llms.txt', 'llms.txt'];
187
+ for (const p of paths)
188
+ if (fs.existsSync(path.join(projectDir, p)))
189
+ return p;
190
+ return null;
191
+ }
192
+ }
@@ -0,0 +1,17 @@
1
+ import { FrameworkAdapter, FrameworkDetectionResult, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
2
+ import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
3
+ export declare class NuxtAdapter implements FrameworkAdapter {
4
+ id: string;
5
+ name: string;
6
+ detect(projectDir: string): boolean;
7
+ detectDetailed(projectDir: string): FrameworkDetectionResult;
8
+ getProjectInfo(projectDir: string): ProjectFrameworkInfo;
9
+ discoverRoutes(projectDir: string): DiscoveredRoute[];
10
+ mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
11
+ findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
12
+ findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
13
+ findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
14
+ findRobotsConfig(projectDir: string): string | null;
15
+ findSitemapConfig(projectDir: string): string | null;
16
+ findLlmsTxt(projectDir: string): string | null;
17
+ }