seo-gravity-mcp 1.1.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 +78 -21
- package/dist/adapters/adapterRegistry.d.ts +1 -0
- package/dist/adapters/adapterRegistry.js +29 -2
- package/dist/adapters/gatsbyAdapter.d.ts +17 -0
- package/dist/adapters/gatsbyAdapter.js +134 -0
- package/dist/adapters/laravelAdapter.d.ts +17 -0
- package/dist/adapters/laravelAdapter.js +192 -0
- package/dist/adapters/nextPagesAdapter.js +19 -0
- package/dist/adapters/nuxtAdapter.d.ts +17 -0
- package/dist/adapters/nuxtAdapter.js +183 -0
- package/dist/adapters/phpClassicAdapter.d.ts +17 -0
- package/dist/adapters/phpClassicAdapter.js +135 -0
- package/dist/adapters/qwikCityAdapter.d.ts +17 -0
- package/dist/adapters/qwikCityAdapter.js +156 -0
- package/dist/adapters/solidStartAdapter.d.ts +17 -0
- package/dist/adapters/solidStartAdapter.js +132 -0
- package/dist/adapters/ssgAdapter.d.ts +17 -0
- package/dist/adapters/ssgAdapter.js +150 -0
- package/dist/adapters/symfonyAdapter.d.ts +17 -0
- package/dist/adapters/symfonyAdapter.js +136 -0
- package/dist/adapters/tanStackRouterAdapter.d.ts +17 -0
- package/dist/adapters/tanStackRouterAdapter.js +179 -0
- package/dist/adapters/types.d.ts +6 -0
- package/dist/adapters/viteReactAdapter.js +17 -0
- package/dist/adapters/wordpressAdapter.d.ts +17 -0
- package/dist/adapters/wordpressAdapter.js +184 -0
- package/dist/benchmark/correlationBenchmark.d.ts +1 -0
- package/dist/benchmark/correlationBenchmark.js +183 -0
- package/dist/benchmark/invariantBenchmark.d.ts +1 -0
- package/dist/benchmark/invariantBenchmark.js +47 -0
- package/dist/cli.d.ts +7 -1
- package/dist/cli.js +141 -76
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8 -646
- package/dist/invariants/registry.d.ts +12 -0
- package/dist/invariants/registry.js +178 -0
- package/dist/invariants/types.d.ts +38 -0
- package/dist/invariants/types.js +1 -0
- package/dist/server/registry.d.ts +3 -0
- package/dist/server/registry.js +559 -0
- package/dist/server/server.d.ts +70 -0
- package/dist/server/server.js +53 -0
- package/dist/server/types.d.ts +7 -0
- package/dist/server/types.js +1 -0
- package/dist/test.js +55 -56
- package/dist/types/findings.d.ts +1 -1
- package/dist/utils/cacheManager.d.ts +15 -1
- package/dist/utils/cacheManager.js +24 -1
- package/dist/utils/gitDiffEngine.d.ts +9 -0
- package/dist/utils/gitDiffEngine.js +38 -1
- package/dist/utils/sarifExporter.d.ts +64 -0
- package/dist/utils/sarifExporter.js +81 -0
- package/dist/utils/snapshotEngine.js +57 -39
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
# 🚀 SEO Gravity (v1.1
|
|
1
|
+
# 🚀 SEO Gravity (v1.2.1)
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/seo-gravity-mcp)
|
|
6
6
|
[](https://www.npmjs.com/package/seo-gravity-mcp)
|
|
7
7
|

|
|
8
|
+

|
|
8
9
|

|
|
10
|
+

|
|
9
11
|

|
|
10
12
|

|
|
11
13
|
|
|
12
|
-
**The
|
|
14
|
+
**The SEO Engineering Infrastructure Layer for AI Coding Agents & CI/CD Pipelines.**
|
|
13
15
|
|
|
14
|
-
[⚡ Instant Start](#-instant-start) • [💻 CLI & CI/CD](#-command-line--cicd-runner) • [🏛 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)
|
|
15
17
|
|
|
16
18
|
</div>
|
|
17
19
|
|
|
@@ -19,14 +21,14 @@
|
|
|
19
21
|
|
|
20
22
|
## 🌟 What is SEO Gravity?
|
|
21
23
|
|
|
22
|
-
**SEO Gravity** is an **SEO engineering infrastructure layer** designed specifically for AI coding agents (Antigravity, Cursor, Claude Code, Windsurf, Cline, Roo Code
|
|
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.
|
|
23
25
|
|
|
24
|
-
Instead of operating as a passive
|
|
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.
|
|
25
27
|
|
|
26
28
|
```text
|
|
27
29
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
28
30
|
│ 1. DISCOVER │ ──▶ │ 2. OBSERVE │ ──▶ │ 3. DIAGNOSE │ ──▶ │ 4. FIX │ ──▶ │ 5. VERIFY │
|
|
29
|
-
│ Framework
|
|
31
|
+
│ 17 Framework │ │ Live/Dev SSR │ │ AST Line Nos │ │ AI Modifies │ │ Invariant │
|
|
30
32
|
│ Adapters │ │ Crawl Graph │ │ Prioritized │ │ Components │ │ Regression │
|
|
31
33
|
│ Source Map │ │ Provenance │ │ Action Plan │ │ Snippets │ │ Git Stamping │
|
|
32
34
|
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
|
@@ -51,33 +53,62 @@ Instead of operating as a passive SEO reporting tool, SEO Gravity connects **ren
|
|
|
51
53
|
│ Observations ≠ Findings; AST inspection, headers, DOM │
|
|
52
54
|
├─────────────────────────────────────────────────────────────┤
|
|
53
55
|
│ 5. PROVIDER & ADAPTER LAYER │
|
|
54
|
-
│ Framework Adapters (
|
|
56
|
+
│ 17 Framework Adapters (JS/TS/PHP/SSG), Providers, Cache │
|
|
55
57
|
└─────────────────────────────────────────────────────────────┘
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
---
|
|
59
61
|
|
|
60
|
-
##
|
|
62
|
+
## 🛡️ Formal SEO Invariant Registry
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
SEO Gravity enforces strict **semantic invariants** rather than heuristic score drops. A regression occurs only when a previously satisfied invariant is violated:
|
|
65
|
+
|
|
66
|
+
| Invariant ID | Name | Target Scope | Description |
|
|
63
67
|
| :--- | :--- | :--- | :--- |
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
68
|
+
| `INV-HTTP-200` | HTTP Success Status | Route | Published routes must return HTTP 200 OK without 4xx/5xx errors. |
|
|
69
|
+
| `INV-CANONICAL-RESOLVES` | Canonical URL Declaration | Page | Indexable pages must declare a canonical URL to prevent duplicate indexing. |
|
|
70
|
+
| `INV-TITLE-PRESENT` | Title Tag Metadata | Page | Pages must declare unique, descriptive title metadata in AST components. |
|
|
71
|
+
| `INV-LINK-ACCESSIBLE` | Link Reachability | Crawl Graph | Public pages must be reachable via internal links (0 orphan pages). |
|
|
72
|
+
| `INV-ROBOTS-ALLOWED` | Robots Configuration | Site | Project must provide `robots.txt` or `robots.ts` specifying bot directives. |
|
|
73
|
+
| `INV-SITEMAP-PRESENT` | XML Sitemap Configuration | Site | Project must publish `sitemap.xml` or dynamic `sitemap.ts`. |
|
|
74
|
+
| `INV-LLMS-TXT` | AI Context Documentation | Site | Project should provide `/llms.txt` for AI agent discovery. |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 🧩 17 Modular Framework Adapters & Benchmark
|
|
79
|
+
|
|
80
|
+
Tested with **100% correlation accuracy** across 17 web application frameworks and CMS ecosystems:
|
|
81
|
+
|
|
82
|
+
| Category | Framework Adapter | Detection Signature | Routing & Template Model | Benchmark |
|
|
83
|
+
| :--- | :--- | :--- | :--- | :---: |
|
|
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%** |
|
|
71
101
|
|
|
72
102
|
---
|
|
73
103
|
|
|
74
|
-
## 💻 Command-Line & CI/CD Runner
|
|
104
|
+
## 💻 Command-Line & CI/CD Runner (SARIF Ready)
|
|
75
105
|
|
|
76
|
-
SEO Gravity includes a standalone CLI binary (`seo-gravity`)
|
|
106
|
+
SEO Gravity includes a standalone CLI binary (`seo-gravity`) with native **SARIF v2.1.0** export for GitHub Code Scanning:
|
|
77
107
|
|
|
78
108
|
```bash
|
|
79
|
-
# 1. Audit project
|
|
109
|
+
# 1. Audit project (Terminal / JSON / SARIF)
|
|
80
110
|
npx seo-gravity-mcp audit --project ./my-app
|
|
111
|
+
npx seo-gravity-mcp audit --project ./my-app --format sarif --output seo-findings.sarif
|
|
81
112
|
|
|
82
113
|
# 2. Create baseline snapshot
|
|
83
114
|
npx seo-gravity-mcp snapshot --project ./my-app --output baseline.json
|
|
@@ -85,10 +116,36 @@ npx seo-gravity-mcp snapshot --project ./my-app --output baseline.json
|
|
|
85
116
|
# 3. Check for SEO regressions in CI/CD PR gate (Exits with 0 or 1)
|
|
86
117
|
npx seo-gravity-mcp check --project ./my-app --baseline baseline.json
|
|
87
118
|
|
|
88
|
-
# 4. Targeted Git differential audit on modified routes only
|
|
119
|
+
# 4. Targeted Semantic Git differential audit on modified routes only
|
|
89
120
|
npx seo-gravity-mcp diff --project ./my-app --base-ref origin/main
|
|
90
121
|
```
|
|
91
122
|
|
|
123
|
+
### GitHub Actions CI/CD Integration
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
name: SEO Quality Gate
|
|
127
|
+
|
|
128
|
+
on: [push, pull_request]
|
|
129
|
+
|
|
130
|
+
jobs:
|
|
131
|
+
seo-check:
|
|
132
|
+
runs-on: ubuntu-latest
|
|
133
|
+
steps:
|
|
134
|
+
- uses: actions/checkout@v4
|
|
135
|
+
- uses: actions/setup-node@v4
|
|
136
|
+
with:
|
|
137
|
+
node-version: 20
|
|
138
|
+
|
|
139
|
+
- name: Run SEO Invariant Regression Check
|
|
140
|
+
run: npx seo-gravity-mcp check --project . --baseline .seo-baseline.json --format sarif --output results.sarif
|
|
141
|
+
|
|
142
|
+
- name: Upload SARIF to GitHub Security Tab
|
|
143
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
144
|
+
if: always()
|
|
145
|
+
with:
|
|
146
|
+
sarif_file: results.sarif
|
|
147
|
+
```
|
|
148
|
+
|
|
92
149
|
---
|
|
93
150
|
|
|
94
151
|
## ⚡ Instant Start (MCP Server)
|
|
@@ -152,4 +209,4 @@ npx -y seo-gravity-mcp
|
|
|
152
209
|
|
|
153
210
|
## 📄 License
|
|
154
211
|
|
|
155
|
-
MIT License. Free for open-source and commercial use.
|
|
212
|
+
MIT License. Copyright (c) 2026 thedevbob005. Free for open-source and commercial use.
|
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -116,6 +116,25 @@ export class NextPagesAdapter {
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
for (const r of routes) {
|
|
120
|
+
if (!r.isDynamic)
|
|
121
|
+
continue;
|
|
122
|
+
const regexStr = '^' + r.routePath
|
|
123
|
+
.replace(/\[\.\.\.([a-zA-Z0-9_-]+)\]/g, '.*')
|
|
124
|
+
.replace(/\[([a-zA-Z0-9_-]+)\]/g, '[^/]+') + '$';
|
|
125
|
+
try {
|
|
126
|
+
if (new RegExp(regexStr).test(urlPath)) {
|
|
127
|
+
return {
|
|
128
|
+
urlPath,
|
|
129
|
+
matchedRoute: r,
|
|
130
|
+
sourceFilePath: r.sourceFilePath,
|
|
131
|
+
confidence: 0.95,
|
|
132
|
+
resolutionMethod: 'pattern_match'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch { }
|
|
137
|
+
}
|
|
119
138
|
return {
|
|
120
139
|
urlPath,
|
|
121
140
|
confidence: 0.0,
|
|
@@ -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
|
+
}
|