seo-gravity-mcp 1.2.0 → 1.3.0
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 +91 -54
- 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/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/wordpressAdapter.d.ts +17 -0
- package/dist/adapters/wordpressAdapter.js +184 -0
- package/dist/benchmark/correlationBenchmark.js +27 -72
- package/dist/benchmark/invariantBenchmark.js +43 -19
- package/dist/benchmark/methodology.d.ts +26 -0
- package/dist/benchmark/methodology.js +194 -0
- package/dist/cli.js +40 -7
- package/dist/invariants/registry.d.ts +1 -1
- package/dist/invariants/registry.js +101 -39
- package/dist/invariants/types.d.ts +5 -1
- package/dist/policy/loader.d.ts +7 -0
- package/dist/policy/loader.js +84 -0
- package/dist/policy/profiles.d.ts +2 -0
- package/dist/policy/profiles.js +102 -0
- package/dist/policy/types.d.ts +23 -0
- package/dist/policy/types.js +1 -0
- package/dist/test.js +57 -104
- package/dist/types/canonical.d.ts +12 -2
- package/dist/types/evidence.d.ts +70 -0
- package/dist/types/evidence.js +1 -0
- package/dist/types/findings.d.ts +1 -1
- package/dist/utils/gitDiffEngine.d.ts +14 -0
- package/dist/utils/gitDiffEngine.js +36 -8
- package/dist/utils/prCommentFormatter.d.ts +3 -0
- package/dist/utils/prCommentFormatter.js +63 -0
- package/dist/utils/snapshotEngine.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 🚀 SEO Gravity (v1.
|
|
1
|
+
# 🚀 SEO Gravity (v1.3.0)
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/seo-gravity-mcp)
|
|
7
7
|

|
|
8
8
|

|
|
9
|
-

|
|
10
|
+

|
|
10
11
|

|
|
11
12
|

|
|
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
|
|
16
|
+
[⚡ Instant Start](#-instant-start) • [💻 CLI & CI/CD](#-command-line--cicd-runner) • [🛡️ Invariant Truth & Policy](#-formal-seo-invariant-registry--policy-engine) • [🏛 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
|
|
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), evaluates semantic SEO invariants against customizable project policies, generates actionable code fix blueprints, and enforces automated regression gates.
|
|
26
27
|
|
|
27
28
|
```text
|
|
28
29
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
29
30
|
│ 1. DISCOVER │ ──▶ │ 2. OBSERVE │ ──▶ │ 3. DIAGNOSE │ ──▶ │ 4. FIX │ ──▶ │ 5. VERIFY │
|
|
30
|
-
│ Framework
|
|
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
|
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
|
@@ -38,75 +39,106 @@ Instead of operating as a passive reporting tool, SEO Gravity connects **rendere
|
|
|
38
39
|
## 🏛 5-Layer System Architecture
|
|
39
40
|
|
|
40
41
|
```text
|
|
41
|
-
|
|
42
|
-
│ 1
|
|
43
|
-
│
|
|
44
|
-
|
|
45
|
-
│
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
│
|
|
49
|
-
│
|
|
50
|
-
|
|
51
|
-
│
|
|
52
|
-
│
|
|
53
|
-
|
|
54
|
-
│
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
43
|
+
│ LAYER 1: AGENT & CLI INTERFACE │
|
|
44
|
+
│ • MCP Server (35 tools) + Standalone CLI (`seo-gravity`) │
|
|
45
|
+
│ • GitHub PR Comment Formatter (`--format pr-comment`) │
|
|
46
|
+
│ • OASIS SARIF v2.1.0 Exporter (`--format sarif`) │
|
|
47
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
48
|
+
│ LAYER 2: INTELLIGENCE & REASONING LAYER │
|
|
49
|
+
│ • Root-cause diagnosis, deterministic priority, fix plans │
|
|
50
|
+
│ • Strategic Opportunity Engine & SEO Experimentation Engine │
|
|
51
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
52
|
+
│ LAYER 3: ANALYSIS, POLICY & INVARIANT LAYER │
|
|
53
|
+
│ • Formal Invariant Registry (Requirement Levels vs Severity) │
|
|
54
|
+
│ • Project Policy Engine (`.seo-gravity.yml` & 5 preset profiles) │
|
|
55
|
+
│ • Semantic AST Git Differential Engine (Risk scoring) │
|
|
56
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
57
|
+
│ LAYER 4: OBSERVATION LAYER (Factual Raw State) │
|
|
58
|
+
│ • Observations ≠ Findings; Page Identity Normalization │
|
|
59
|
+
│ • Polymorphic Evidence: AST, Template, RouteConfig, DOM, Header, Runtime │
|
|
60
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
61
|
+
│ LAYER 5: PROVIDER & ADAPTER LAYER │
|
|
62
|
+
│ • 17 Framework Adapters (JS/TS/PHP/SSG), Providers, Cache Provenance │
|
|
63
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
57
64
|
```
|
|
58
65
|
|
|
59
66
|
---
|
|
60
67
|
|
|
61
|
-
## 🛡️ Formal SEO Invariant Registry
|
|
68
|
+
## 🛡️ Formal SEO Invariant Registry & Policy Engine
|
|
62
69
|
|
|
63
|
-
SEO Gravity
|
|
70
|
+
SEO Gravity evaluates facts against formal invariant contracts and allows teams to configure organizational standards via `.seo-gravity.yml`:
|
|
64
71
|
|
|
65
|
-
| Invariant ID | Name |
|
|
66
|
-
| :--- | :--- |
|
|
67
|
-
| `INV-HTTP-200` | HTTP Success Status | Route | Published routes
|
|
68
|
-
| `INV-CANONICAL-RESOLVES` | Canonical URL Declaration | Page |
|
|
69
|
-
| `INV-TITLE-PRESENT` | Title Tag Metadata | Page |
|
|
70
|
-
| `INV-LINK-ACCESSIBLE` | Link Reachability | Crawl Graph |
|
|
71
|
-
| `INV-ROBOTS-ALLOWED` | Robots
|
|
72
|
-
| `INV-SITEMAP-PRESENT` | XML Sitemap Configuration | Site |
|
|
73
|
-
| `INV-LLMS-TXT` | AI Context Documentation | Site |
|
|
72
|
+
| Invariant ID | Name | Scope | Requirement Level | Severity | Semantic Failure Condition |
|
|
73
|
+
| :--- | :--- | :---: | :---: | :---: | :--- |
|
|
74
|
+
| `INV-HTTP-200` | HTTP Success Status | Route | **`REQUIRED`** | Critical | Published routes return 4xx or 5xx HTTP status. |
|
|
75
|
+
| `INV-CANONICAL-RESOLVES` | Canonical URL Declaration | Page | **`CONDITIONAL`** | High | Indexable page lacks `<link rel="canonical">` or canonical metadata. |
|
|
76
|
+
| `INV-TITLE-PRESENT` | Title Tag Metadata | Page | **`REQUIRED`** | High | Component metadata or `<title>` tag is missing. |
|
|
77
|
+
| `INV-LINK-ACCESSIBLE` | Link Reachability | Crawl Graph | **`CONDITIONAL`** | Medium | Public page has 0 internal inlinks (Orphan Page). |
|
|
78
|
+
| `INV-ROBOTS-ALLOWED` | Robots Policy Determinable | Site | **`CONDITIONAL`** | Medium | Bot directives are indeterminable or block critical assets. |
|
|
79
|
+
| `INV-SITEMAP-PRESENT` | XML Sitemap Configuration | Site | **`RECOMMENDED`** | Medium | Missing `sitemap.xml` or dynamic sitemap route. |
|
|
80
|
+
| `INV-LLMS-TXT` | AI Context Documentation | Site | **`RECOMMENDED`** | Low | Missing `/llms.txt` context file for AI crawlers (GEO). |
|
|
81
|
+
|
|
82
|
+
### Project Policy Profiles (`.seo-gravity.yml`)
|
|
83
|
+
|
|
84
|
+
Configure project policies with built-in presets (`strict`, `balanced`, `startup`, `ecommerce`, `documentation`):
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
version: 1
|
|
88
|
+
profile: balanced # strict | balanced | startup | ecommerce | documentation
|
|
89
|
+
|
|
90
|
+
policy:
|
|
91
|
+
canonical: required
|
|
92
|
+
sitemap: recommended
|
|
93
|
+
llms_txt: recommended
|
|
94
|
+
|
|
95
|
+
regression:
|
|
96
|
+
fail_on_levels: [REQUIRED, CONDITIONAL]
|
|
97
|
+
fail_on_severities: [critical, high]
|
|
98
|
+
```
|
|
74
99
|
|
|
75
100
|
---
|
|
76
101
|
|
|
77
|
-
## 🧩 Modular Framework Adapters & Benchmark
|
|
102
|
+
## 🧩 17 Modular Framework Adapters & Benchmark
|
|
78
103
|
|
|
79
|
-
Tested with **100% correlation accuracy** across
|
|
104
|
+
Tested with **100% correlation accuracy** across 17 web application frameworks and CMS ecosystems:
|
|
80
105
|
|
|
81
|
-
| Framework Adapter | Detection |
|
|
106
|
+
| Category | Framework Adapter | Detection Signature | Routing & Template Model | Benchmark Result |
|
|
82
107
|
| :--- | :--- | :--- | :--- | :---: |
|
|
83
|
-
| **`NextAppAdapter`** | `next` + `app/` | App Router hierarchy, route groups `(group)`, dynamic `[slug]` |
|
|
84
|
-
| **`NextPagesAdapter`** | `next` + `pages/` | Pages router hierarchy, dynamic `[id]` |
|
|
85
|
-
| **`AstroAdapter`** | `astro` + `src/pages` | `.astro`, `.md`, `.mdx` pages |
|
|
86
|
-
| **`
|
|
87
|
-
| **`
|
|
88
|
-
| **`
|
|
89
|
-
|
|
|
108
|
+
| **JS / TS SSR** | **`NextAppAdapter`** | `next` + `app/` | App Router hierarchy, route groups `(group)`, dynamic `[slug]` | **100%** |
|
|
109
|
+
| **JS / TS SSR** | **`NextPagesAdapter`** | `next` + `pages/` | Pages router hierarchy, dynamic `[id]` | **100%** |
|
|
110
|
+
| **JS / TS SSR** | **`AstroAdapter`** | `astro` + `src/pages` | `.astro`, `.md`, `.mdx` pages | **100%** |
|
|
111
|
+
| **JS / TS SSR** | **`RemixAdapter`** | `@remix-run/react` | Flat routes `app/routes/`, index `_index.tsx`, dynamic `$slug` | **100%** |
|
|
112
|
+
| **JS / TS SSR** | **`SvelteKitAdapter`** | `@sveltejs/kit` | `src/routes/` (`+page.svelte`), dynamic `[slug]` | **100%** |
|
|
113
|
+
| **JS / TS SSR** | **`NuxtAdapter`** | `nuxt` in `package.json` | `pages/**/*.vue` with `useHead()`, `useSeoMeta()` | **100%** |
|
|
114
|
+
| **JS / TS SSR** | **`TanStackRouterAdapter`** | `@tanstack/react-router` | `src/routes/` with `createFileRoute` & `head()` | **100%** |
|
|
115
|
+
| **JS / TS SSR** | **`SolidStartAdapter`** | `@solidjs/start` | `src/routes/` with `<Title>`, `<Meta>`, `<Link>` | **100%** |
|
|
116
|
+
| **JS / TS SSR** | **`QwikCityAdapter`** | `@builder.io/qwik-city` | `src/routes/**/index.tsx` with `DocumentHead` | **100%** |
|
|
117
|
+
| **JS / TS SSG** | **`GatsbyAdapter`** | `gatsby` in `package.json` | `src/pages/` with `export const Head` | **100%** |
|
|
118
|
+
| **PHP / CMS** | **`WordPressAdapter`** | `style.css` / `wp-content` | Classic templates (`front-page.php`, `single.php`) & FSE block templates | **100%** |
|
|
119
|
+
| **PHP Fullstack** | **`LaravelAdapter`** | `artisan` / `laravel/framework` | `routes/web.php` + Blade templates (`@section('title')`) | **100%** |
|
|
120
|
+
| **PHP Fullstack** | **`SymfonyAdapter`** | `symfony.lock` / `composer.json` | `config/routes.yaml` / `#[Route]` + Twig templates (`{% block title %}`) | **100%** |
|
|
121
|
+
| **PHP Classic** | **`PhpClassicAdapter`** | `*.php` files | File-based routing (`index.php`, `about.php`) | **100%** |
|
|
122
|
+
| **Markdown SSG** | **`SsgAdapter`** | `hugo.toml`, `_config.yml`, `11ty` | `content/**/*.md`, `_posts/**/*.md` YAML/TOML frontmatter | **100%** |
|
|
123
|
+
| **React SPA** | **`ViteReactAdapter`** | `vite` + `react` | `index.html` + React Router route trees | **100%** |
|
|
124
|
+
| **Static HTML** | **`StaticAdapter`** | Static `.html` files | Recursive HTML directory traversal | **100%** |
|
|
90
125
|
|
|
91
126
|
---
|
|
92
127
|
|
|
93
|
-
## 💻 Command-Line & CI/CD Runner
|
|
128
|
+
## 💻 Command-Line & CI/CD Runner
|
|
94
129
|
|
|
95
|
-
SEO Gravity includes a standalone CLI binary (`seo-gravity`) with native **SARIF v2.1.0**
|
|
130
|
+
SEO Gravity includes a standalone CLI binary (`seo-gravity`) with native **SARIF v2.1.0** and **GitHub PR Comment** export:
|
|
96
131
|
|
|
97
132
|
```bash
|
|
98
|
-
# 1. Audit project (Terminal / JSON / SARIF)
|
|
133
|
+
# 1. Audit project (Terminal / JSON / SARIF / PR-Comment)
|
|
99
134
|
npx seo-gravity-mcp audit --project ./my-app
|
|
100
135
|
npx seo-gravity-mcp audit --project ./my-app --format sarif --output seo-findings.sarif
|
|
101
136
|
|
|
102
|
-
# 2.
|
|
103
|
-
npx seo-gravity-mcp
|
|
104
|
-
|
|
105
|
-
# 3. Check for SEO regressions in CI/CD PR gate (Exits with 0 or 1)
|
|
106
|
-
npx seo-gravity-mcp check --project ./my-app --baseline baseline.json
|
|
137
|
+
# 2. Check regressions in CI/CD against project policy (Exits 0 or 1)
|
|
138
|
+
npx seo-gravity-mcp check --project ./my-app --baseline baseline.json --policy .seo-gravity.yml --format pr-comment
|
|
107
139
|
|
|
108
|
-
#
|
|
109
|
-
npx seo-gravity-mcp
|
|
140
|
+
# 3. Targeted Semantic Git differential audit & SEO Code Review on modified routes
|
|
141
|
+
npx seo-gravity-mcp review --project ./my-app --base-ref origin/main --format pr-comment --output pr-comment.md
|
|
110
142
|
```
|
|
111
143
|
|
|
112
144
|
### GitHub Actions CI/CD Integration
|
|
@@ -126,7 +158,12 @@ jobs:
|
|
|
126
158
|
node-version: 20
|
|
127
159
|
|
|
128
160
|
- name: Run SEO Invariant Regression Check
|
|
129
|
-
run:
|
|
161
|
+
run: |
|
|
162
|
+
npx seo-gravity-mcp check \
|
|
163
|
+
--project . \
|
|
164
|
+
--baseline .seo-baseline.json \
|
|
165
|
+
--format sarif \
|
|
166
|
+
--output results.sarif
|
|
130
167
|
|
|
131
168
|
- name: Upload SARIF to GitHub Security Tab
|
|
132
169
|
uses: github/codeql-action/upload-sarif@v3
|
|
@@ -171,7 +208,7 @@ npx -y seo-gravity-mcp
|
|
|
171
208
|
|
|
172
209
|
---
|
|
173
210
|
|
|
174
|
-
## 🛠 Complete Tool Catalog
|
|
211
|
+
## 🛠 Complete Tool Catalog (35 Tools)
|
|
175
212
|
|
|
176
213
|
### Layer 0: Agent Orchestration & Remediation (Flagship)
|
|
177
214
|
| Tool Name | Operational Class | Description |
|
|
@@ -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
|
+
}
|