seo-gravity-mcp 1.0.1 → 1.1.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 +144 -9
- package/dist/adapters/adapterRegistry.d.ts +7 -0
- package/dist/adapters/adapterRegistry.js +30 -0
- package/dist/adapters/astroAdapter.d.ts +17 -0
- package/dist/adapters/astroAdapter.js +191 -0
- package/dist/adapters/nextAppAdapter.d.ts +17 -0
- package/dist/adapters/nextAppAdapter.js +265 -0
- package/dist/adapters/nextPagesAdapter.d.ts +17 -0
- package/dist/adapters/nextPagesAdapter.js +190 -0
- package/dist/adapters/remixAdapter.d.ts +16 -0
- package/dist/adapters/remixAdapter.js +118 -0
- package/dist/adapters/staticAdapter.d.ts +16 -0
- package/dist/adapters/staticAdapter.js +117 -0
- package/dist/adapters/svelteKitAdapter.d.ts +16 -0
- package/dist/adapters/svelteKitAdapter.js +122 -0
- package/dist/adapters/types.d.ts +33 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/viteReactAdapter.d.ts +16 -0
- package/dist/adapters/viteReactAdapter.js +172 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +97 -0
- package/dist/index.js +140 -10
- package/dist/providers/providerRegistry.d.ts +65 -0
- package/dist/providers/providerRegistry.js +64 -0
- package/dist/providers/types.d.ts +22 -0
- package/dist/providers/types.js +1 -0
- package/dist/test.js +121 -59
- package/dist/tools/geo.js +36 -0
- package/dist/tools/orchestration.d.ts +29 -0
- package/dist/tools/orchestration.js +304 -0
- package/dist/types/canonical.d.ts +64 -0
- package/dist/types/canonical.js +1 -0
- package/dist/types/findings.d.ts +246 -0
- package/dist/types/findings.js +1 -0
- package/dist/types/seo.d.ts +39 -0
- package/dist/types/seo.js +1 -1
- package/dist/utils/astLocator.d.ts +14 -0
- package/dist/utils/astLocator.js +137 -0
- package/dist/utils/cacheManager.d.ts +15 -0
- package/dist/utils/cacheManager.js +37 -0
- package/dist/utils/crawlGraph.d.ts +19 -0
- package/dist/utils/crawlGraph.js +231 -0
- package/dist/utils/experimentEngine.d.ts +26 -0
- package/dist/utils/experimentEngine.js +41 -0
- package/dist/utils/findingEngine.d.ts +43 -0
- package/dist/utils/findingEngine.js +272 -0
- package/dist/utils/gitDiffEngine.d.ts +19 -0
- package/dist/utils/gitDiffEngine.js +72 -0
- package/dist/utils/opportunityEngine.d.ts +19 -0
- package/dist/utils/opportunityEngine.js +61 -0
- package/dist/utils/projectScanner.d.ts +6 -0
- package/dist/utils/projectScanner.js +21 -0
- package/dist/utils/snapshotEngine.d.ts +11 -0
- package/dist/utils/snapshotEngine.js +405 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,20 +1,155 @@
|
|
|
1
|
-
# 🚀 SEO Gravity
|
|
1
|
+
# 🚀 SEO Gravity (v1.1.0)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/seo-gravity-mcp)
|
|
6
|
+
[](https://www.npmjs.com/package/seo-gravity-mcp)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
**The Enterprise SEO Engineering Layer for AI Coding Agents & CI/CD Pipelines.**
|
|
13
|
+
|
|
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)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🌟 What is SEO Gravity?
|
|
21
|
+
|
|
22
|
+
**SEO Gravity** is an **SEO engineering infrastructure layer** designed specifically for AI coding agents (Antigravity, Cursor, Claude Code, Windsurf, Cline, Roo Code, etc.) and automated CI/CD pipelines.
|
|
23
|
+
|
|
24
|
+
Instead of operating as a passive SEO 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.
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
28
|
+
│ 1. DISCOVER │ ──▶ │ 2. OBSERVE │ ──▶ │ 3. DIAGNOSE │ ──▶ │ 4. FIX │ ──▶ │ 5. VERIFY │
|
|
29
|
+
│ Framework │ │ Live/Dev SSR │ │ AST Line Nos │ │ AI Modifies │ │ Invariant │
|
|
30
|
+
│ Adapters │ │ Crawl Graph │ │ Prioritized │ │ Components │ │ Regression │
|
|
31
|
+
│ Source Map │ │ Provenance │ │ Action Plan │ │ Snippets │ │ Git Stamping │
|
|
32
|
+
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🏛 5-Layer System Architecture
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
41
|
+
│ 1. AGENT & CLI INTERFACE │
|
|
42
|
+
│ MCP Server (35 tools) + Standalone CLI (`seo-gravity`) │
|
|
43
|
+
├─────────────────────────────────────────────────────────────┤
|
|
44
|
+
│ 2. INTELLIGENCE & REASONING LAYER │
|
|
45
|
+
│ Root-cause diagnosis, deterministic priority, fix plans │
|
|
46
|
+
├─────────────────────────────────────────────────────────────┤
|
|
47
|
+
│ 3. ANALYSIS & INVARIANT LAYER │
|
|
48
|
+
│ SEO Invariants (HTTP 200, canonical, schema, noindex) │
|
|
49
|
+
├─────────────────────────────────────────────────────────────┤
|
|
50
|
+
│ 4. OBSERVATION LAYER (Factual Raw State) │
|
|
51
|
+
│ Observations ≠ Findings; AST inspection, headers, DOM │
|
|
52
|
+
├─────────────────────────────────────────────────────────────┤
|
|
53
|
+
│ 5. PROVIDER & ADAPTER LAYER │
|
|
54
|
+
│ Framework Adapters (Next, Astro, Vite), Providers, Cache │
|
|
55
|
+
└─────────────────────────────────────────────────────────────┘
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🧩 Modular Framework Adapters
|
|
61
|
+
|
|
62
|
+
| Framework Adapter | Detection | Route Discovery | AST Inspection |
|
|
63
|
+
| :--- | :--- | :--- | :--- |
|
|
64
|
+
| **`NextAppAdapter`** | `next` + `app/` | App Router hierarchy, route groups `(group)`, dynamic `[slug]` | Pinpoints `metadata` export & `generateMetadata()` lines |
|
|
65
|
+
| **`NextPagesAdapter`** | `next` + `pages/` | Pages router hierarchy, dynamic `[id]` | Scans `<Head>` & `<NextSeo>` JSX components |
|
|
66
|
+
| **`AstroAdapter`** | `astro` + `src/pages` | `.astro`, `.md`, `.mdx` pages | Scans Astro frontmatter & `<title>` tags |
|
|
67
|
+
| **`ViteReactAdapter`** | `vite` / `react` | `index.html` + React Router routes | Scans `<Helmet>` & route definition trees |
|
|
68
|
+
| **`RemixAdapter`** | `@remix-run/react` | `app/routes/` | Inspects `meta` and `links` export functions |
|
|
69
|
+
| **`SvelteKitAdapter`** | `@sveltejs/kit` | `src/routes/` (`+page.svelte`) | Scans `<svelte:head>` components |
|
|
70
|
+
| **`StaticAdapter`** | Static HTML files | Recursive HTML directory traversal | Direct DOM / meta element inspection |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 💻 Command-Line & CI/CD Runner
|
|
75
|
+
|
|
76
|
+
SEO Gravity includes a standalone CLI binary (`seo-gravity`) for direct terminal use and GitHub Actions / CI/CD quality gates:
|
|
6
77
|
|
|
7
78
|
```bash
|
|
8
|
-
|
|
9
|
-
|
|
79
|
+
# 1. Audit project
|
|
80
|
+
npx seo-gravity-mcp audit --project ./my-app
|
|
81
|
+
|
|
82
|
+
# 2. Create baseline snapshot
|
|
83
|
+
npx seo-gravity-mcp snapshot --project ./my-app --output baseline.json
|
|
84
|
+
|
|
85
|
+
# 3. Check for SEO regressions in CI/CD PR gate (Exits with 0 or 1)
|
|
86
|
+
npx seo-gravity-mcp check --project ./my-app --baseline baseline.json
|
|
87
|
+
|
|
88
|
+
# 4. Targeted Git differential audit on modified routes only
|
|
89
|
+
npx seo-gravity-mcp diff --project ./my-app --base-ref origin/main
|
|
10
90
|
```
|
|
11
91
|
|
|
12
|
-
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## ⚡ Instant Start (MCP Server)
|
|
13
95
|
|
|
14
96
|
```bash
|
|
15
|
-
|
|
97
|
+
npx -y seo-gravity-mcp
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### AI Client Configurations
|
|
101
|
+
|
|
102
|
+
#### Cursor (`.cursor/mcp.json`)
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"seo-gravity": {
|
|
107
|
+
"command": "npx",
|
|
108
|
+
"args": ["-y", "seo-gravity-mcp"]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Claude Desktop (`claude_desktop_config.json`)
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"mcpServers": {
|
|
118
|
+
"seo-gravity": {
|
|
119
|
+
"command": "npx",
|
|
120
|
+
"args": ["-y", "seo-gravity-mcp"]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
16
124
|
```
|
|
17
125
|
|
|
18
|
-
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 🛠 Complete Tool Catalog
|
|
129
|
+
|
|
130
|
+
### Layer 0: Agent Orchestration & Remediation (Flagship)
|
|
131
|
+
| Tool Name | Operational Class | Description |
|
|
132
|
+
| :--- | :--- | :--- |
|
|
133
|
+
| `seo_project_audit` | **Planning / Audit** | Scans workspace framework, discovers routes, correlates source files, builds crawl graph, and computes multidimensional SEO health. |
|
|
134
|
+
| `seo_diagnose` | **Planning / Diagnose** | Deep root-cause AST inspection linking an observed issue/URL back to specific source code line ranges with ready-to-use fix blueprints. |
|
|
135
|
+
| `seo_prioritize` | **Planning / Strategy** | Ranks findings into Quick Wins, Critical Blockers, and Architectural Improvements using `(Impact × Confidence × Reach) / Effort`. |
|
|
136
|
+
| `seo_fix_plan` | **Planning / Execution** | Generates an end-to-end code modification and verification plan for AI coding agents. |
|
|
137
|
+
| `seo_snapshot_create` | **Verification** | Creates and saves a project SEO baseline snapshot with Git commit stamping adhering to schema `seo.gravity/v1`. |
|
|
138
|
+
| `seo_snapshot_compare` | **Verification** | Invariant-based diffing of two snapshots to isolate resolved findings, new regressions, and multidimensional score deltas. |
|
|
139
|
+
| `seo_regression_check` | **Verification / CI** | Automated pass/fail regression check with alert messages for CI/CD gates and PR bots. |
|
|
140
|
+
|
|
141
|
+
### Layers 1–8: Specialized SEO & GEO Engines
|
|
142
|
+
- **Layer 1 (SERP & Competitors)**: `seo_serp_analyze`, `seo_competitor_content_gap`, `seo_competitor_profile`, `seo_competitor_diff`, `seo_forum_discussions_pulse`
|
|
143
|
+
- **Layer 2 (GEO & AI Search)**: `seo_geo_ai_readiness_audit`, `seo_llms_txt_generate`, `seo_ai_bots_robots_audit`
|
|
144
|
+
- **Layer 3 (Info Gain & E-E-A-T)**: `seo_information_gain_score`, `seo_eeat_audit`
|
|
145
|
+
- **Layer 4 (On-Page)**: `seo_onpage_audit`, `seo_content_brief_generate`, `seo_readability_score`
|
|
146
|
+
- **Layer 5 (Technical SEO)**: `seo_technical_audit`, `seo_js_rendering_diff`, `seo_robots_txt_validate`, `seo_sitemap_inspect`, `seo_internal_links_analyze`
|
|
147
|
+
- **Layer 6 (Keywords & Intent)**: `seo_keyword_suggestions`, `seo_questions_find`, `seo_keyword_cluster`, `seo_search_intent_classify`
|
|
148
|
+
- **Layer 7 (Schema & Entity)**: `seo_entity_salience_map`, `seo_schema_generate`, `seo_schema_validate`
|
|
149
|
+
- **Layer 8 (Speed & Maintenance)**: `seo_pagespeed_audit`, `seo_indexnow_submit`, `seo_content_decay_audit`
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 📄 License
|
|
19
154
|
|
|
20
|
-
|
|
155
|
+
MIT License. Free for open-source and commercial use.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FrameworkAdapter } from './types.js';
|
|
2
|
+
export declare class AdapterRegistry {
|
|
3
|
+
private adapters;
|
|
4
|
+
getAdapterForProject(projectDir: string): FrameworkAdapter;
|
|
5
|
+
getAdapterById(id: string): FrameworkAdapter | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare const defaultAdapterRegistry: AdapterRegistry;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NextAppAdapter } from './nextAppAdapter.js';
|
|
2
|
+
import { NextPagesAdapter } from './nextPagesAdapter.js';
|
|
3
|
+
import { AstroAdapter } from './astroAdapter.js';
|
|
4
|
+
import { ViteReactAdapter } from './viteReactAdapter.js';
|
|
5
|
+
import { RemixAdapter } from './remixAdapter.js';
|
|
6
|
+
import { SvelteKitAdapter } from './svelteKitAdapter.js';
|
|
7
|
+
import { StaticAdapter } from './staticAdapter.js';
|
|
8
|
+
export class AdapterRegistry {
|
|
9
|
+
adapters = [
|
|
10
|
+
new NextAppAdapter(),
|
|
11
|
+
new NextPagesAdapter(),
|
|
12
|
+
new AstroAdapter(),
|
|
13
|
+
new RemixAdapter(),
|
|
14
|
+
new SvelteKitAdapter(),
|
|
15
|
+
new ViteReactAdapter(),
|
|
16
|
+
new StaticAdapter() // fallback
|
|
17
|
+
];
|
|
18
|
+
getAdapterForProject(projectDir) {
|
|
19
|
+
for (const adapter of this.adapters) {
|
|
20
|
+
if (adapter.detect(projectDir)) {
|
|
21
|
+
return adapter;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return this.adapters[this.adapters.length - 1]; // fallback static
|
|
25
|
+
}
|
|
26
|
+
getAdapterById(id) {
|
|
27
|
+
return this.adapters.find(a => a.id === id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const defaultAdapterRegistry = new AdapterRegistry();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FrameworkAdapter, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
|
|
2
|
+
import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
|
|
3
|
+
export declare class AstroAdapter implements FrameworkAdapter {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
detect(projectDir: string): boolean;
|
|
7
|
+
getProjectInfo(projectDir: string): ProjectFrameworkInfo;
|
|
8
|
+
discoverRoutes(projectDir: string): DiscoveredRoute[];
|
|
9
|
+
mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
|
|
10
|
+
findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
|
|
11
|
+
findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
|
|
12
|
+
findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
|
|
13
|
+
findRobotsConfig(projectDir: string): string | null;
|
|
14
|
+
findSitemapConfig(projectDir: string): string | null;
|
|
15
|
+
findLlmsTxt(projectDir: string): string | null;
|
|
16
|
+
private extractDynamicParams;
|
|
17
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
export class AstroAdapter {
|
|
4
|
+
id = 'astro';
|
|
5
|
+
name = 'Astro Framework';
|
|
6
|
+
detect(projectDir) {
|
|
7
|
+
const hasConfig = fs.existsSync(path.join(projectDir, 'astro.config.mjs')) ||
|
|
8
|
+
fs.existsSync(path.join(projectDir, 'astro.config.ts'));
|
|
9
|
+
if (hasConfig)
|
|
10
|
+
return true;
|
|
11
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
12
|
+
if (fs.existsSync(pkgPath)) {
|
|
13
|
+
try {
|
|
14
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
15
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
16
|
+
return Boolean(deps['astro']);
|
|
17
|
+
}
|
|
18
|
+
catch { }
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
getProjectInfo(projectDir) {
|
|
23
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
24
|
+
let pkg = {};
|
|
25
|
+
if (fs.existsSync(pkgPath)) {
|
|
26
|
+
try {
|
|
27
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
}
|
|
31
|
+
const hasSrcPages = fs.existsSync(path.join(projectDir, 'src/pages'));
|
|
32
|
+
const routesDir = hasSrcPages ? 'src/pages' : 'pages';
|
|
33
|
+
return {
|
|
34
|
+
framework: 'astro',
|
|
35
|
+
name: pkg.name || path.basename(projectDir),
|
|
36
|
+
version: pkg.dependencies?.astro || pkg.devDependencies?.astro,
|
|
37
|
+
packageManager: fs.existsSync(path.join(projectDir, 'pnpm-lock.yaml'))
|
|
38
|
+
? 'pnpm'
|
|
39
|
+
: fs.existsSync(path.join(projectDir, 'yarn.lock'))
|
|
40
|
+
? 'yarn'
|
|
41
|
+
: 'npm',
|
|
42
|
+
hasTypeScript: fs.existsSync(path.join(projectDir, 'tsconfig.json')),
|
|
43
|
+
hasSitemapConfig: Boolean(this.findSitemapConfig(projectDir)),
|
|
44
|
+
hasRobotsConfig: Boolean(this.findRobotsConfig(projectDir)),
|
|
45
|
+
hasLlmsTxt: Boolean(this.findLlmsTxt(projectDir)),
|
|
46
|
+
rootDir: path.resolve(projectDir),
|
|
47
|
+
routesDir,
|
|
48
|
+
devCommand: 'npm run dev',
|
|
49
|
+
defaultDevPort: 4321
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
discoverRoutes(projectDir) {
|
|
53
|
+
const info = this.getProjectInfo(projectDir);
|
|
54
|
+
const fullRoutesDir = path.join(projectDir, info.routesDir || 'src/pages');
|
|
55
|
+
const discovered = [];
|
|
56
|
+
if (!fs.existsSync(fullRoutesDir))
|
|
57
|
+
return discovered;
|
|
58
|
+
const scanDir = (currentDir, routePrefix) => {
|
|
59
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const entryPath = path.join(currentDir, entry.name);
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
const nextPrefix = routePrefix === '/' ? `/${entry.name}` : `${routePrefix}/${entry.name}`;
|
|
64
|
+
scanDir(entryPath, nextPrefix);
|
|
65
|
+
}
|
|
66
|
+
else if (entry.isFile() && /\.(astro|md|mdx)$/.test(entry.name)) {
|
|
67
|
+
const baseName = entry.name.replace(/\.(astro|md|mdx)$/, '');
|
|
68
|
+
let routePath = routePrefix;
|
|
69
|
+
if (baseName !== 'index') {
|
|
70
|
+
routePath = routePrefix === '/' ? `/${baseName}` : `${routePrefix}/${baseName}`;
|
|
71
|
+
}
|
|
72
|
+
if (routePath === '')
|
|
73
|
+
routePath = '/';
|
|
74
|
+
const relFile = path.relative(projectDir, entryPath).replace(/\\/g, '/');
|
|
75
|
+
const content = fs.readFileSync(entryPath, 'utf-8');
|
|
76
|
+
const dynamicParams = this.extractDynamicParams(routePath);
|
|
77
|
+
discovered.push({
|
|
78
|
+
routePath,
|
|
79
|
+
sourceFilePath: relFile,
|
|
80
|
+
isDynamic: dynamicParams.length > 0,
|
|
81
|
+
dynamicParams,
|
|
82
|
+
routeType: 'page',
|
|
83
|
+
hasMetadataExport: /title\s*=|description\s*=|astro:head|<title/.test(content),
|
|
84
|
+
hasGenerateMetadata: false,
|
|
85
|
+
hasSchemaMarkup: /application\/ld\+json/.test(content),
|
|
86
|
+
hasHeadComponent: /<head|astro:head|<title/.test(content),
|
|
87
|
+
isClientComponent: false
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
scanDir(fullRoutesDir, '/');
|
|
93
|
+
return discovered;
|
|
94
|
+
}
|
|
95
|
+
mapRouteToSource(targetUrl, routes) {
|
|
96
|
+
let urlPath = targetUrl;
|
|
97
|
+
try {
|
|
98
|
+
if (targetUrl.startsWith('http')) {
|
|
99
|
+
urlPath = new URL(targetUrl).pathname;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
urlPath = targetUrl;
|
|
104
|
+
}
|
|
105
|
+
urlPath = urlPath.replace(/\/$/, '') || '/';
|
|
106
|
+
for (const r of routes) {
|
|
107
|
+
const clean = r.routePath.replace(/\/$/, '') || '/';
|
|
108
|
+
if (clean === urlPath) {
|
|
109
|
+
return {
|
|
110
|
+
urlPath,
|
|
111
|
+
matchedRoute: r,
|
|
112
|
+
sourceFilePath: r.sourceFilePath,
|
|
113
|
+
confidence: 1.0,
|
|
114
|
+
resolutionMethod: 'exact_match'
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
urlPath,
|
|
120
|
+
confidence: 0.0,
|
|
121
|
+
resolutionMethod: 'unmapped'
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async findMetadataImplementation(projectDir, route) {
|
|
125
|
+
const fullPath = path.join(projectDir, route.sourceFilePath);
|
|
126
|
+
if (!fs.existsSync(fullPath))
|
|
127
|
+
return { hasMetadata: false, type: 'none' };
|
|
128
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
129
|
+
const hasMeta = /<title\b|<meta\s+name=["']description["']/i.test(content) || /title\s*:/i.test(content);
|
|
130
|
+
return {
|
|
131
|
+
hasMetadata: hasMeta,
|
|
132
|
+
type: hasMeta ? 'jsx_head' : 'none'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async findCanonicalDeclaration(projectDir, route) {
|
|
136
|
+
const fullPath = path.join(projectDir, route.sourceFilePath);
|
|
137
|
+
if (!fs.existsSync(fullPath))
|
|
138
|
+
return { hasCanonical: false, type: 'none' };
|
|
139
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
140
|
+
const hasCanonical = /rel=["']canonical["']/i.test(content);
|
|
141
|
+
return {
|
|
142
|
+
hasCanonical,
|
|
143
|
+
type: hasCanonical ? 'link_jsx_tag' : 'none'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async findSchemaDeclaration(projectDir, route) {
|
|
147
|
+
const fullPath = path.join(projectDir, route.sourceFilePath);
|
|
148
|
+
if (!fs.existsSync(fullPath))
|
|
149
|
+
return { hasSchema: false, typesFound: [] };
|
|
150
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
151
|
+
const hasSchema = /application\/ld\+json/i.test(content);
|
|
152
|
+
return {
|
|
153
|
+
hasSchema,
|
|
154
|
+
typesFound: hasSchema ? ['JSON-LD'] : []
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
findRobotsConfig(projectDir) {
|
|
158
|
+
const paths = ['public/robots.txt', 'robots.txt'];
|
|
159
|
+
for (const p of paths) {
|
|
160
|
+
if (fs.existsSync(path.join(projectDir, p)))
|
|
161
|
+
return p;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
findSitemapConfig(projectDir) {
|
|
166
|
+
const paths = ['public/sitemap.xml', 'sitemap.xml', 'src/pages/sitemap.xml.ts'];
|
|
167
|
+
for (const p of paths) {
|
|
168
|
+
if (fs.existsSync(path.join(projectDir, p)))
|
|
169
|
+
return p;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
findLlmsTxt(projectDir) {
|
|
174
|
+
const paths = ['public/llms.txt', 'llms.txt'];
|
|
175
|
+
for (const p of paths) {
|
|
176
|
+
if (fs.existsSync(path.join(projectDir, p)))
|
|
177
|
+
return p;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
extractDynamicParams(routePath) {
|
|
182
|
+
const params = [];
|
|
183
|
+
const matches = routePath.match(/\[\.{0,3}([a-zA-Z0-9_-]+)\]/g);
|
|
184
|
+
if (matches) {
|
|
185
|
+
for (const m of matches) {
|
|
186
|
+
params.push(m.replace(/[\[\].]/g, ''));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return params;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FrameworkAdapter, MetadataLocationInfo, CanonicalLocationInfo, SchemaLocationInfo } from './types.js';
|
|
2
|
+
import { DiscoveredRoute, ProjectFrameworkInfo, RouteSourceMapping } from '../types/findings.js';
|
|
3
|
+
export declare class NextAppAdapter implements FrameworkAdapter {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
detect(projectDir: string): boolean;
|
|
7
|
+
getProjectInfo(projectDir: string): ProjectFrameworkInfo;
|
|
8
|
+
discoverRoutes(projectDir: string): DiscoveredRoute[];
|
|
9
|
+
mapRouteToSource(targetUrl: string, routes: DiscoveredRoute[]): RouteSourceMapping;
|
|
10
|
+
findMetadataImplementation(projectDir: string, route: DiscoveredRoute): Promise<MetadataLocationInfo>;
|
|
11
|
+
findCanonicalDeclaration(projectDir: string, route: DiscoveredRoute): Promise<CanonicalLocationInfo>;
|
|
12
|
+
findSchemaDeclaration(projectDir: string, route: DiscoveredRoute): Promise<SchemaLocationInfo>;
|
|
13
|
+
findRobotsConfig(projectDir: string): string | null;
|
|
14
|
+
findSitemapConfig(projectDir: string): string | null;
|
|
15
|
+
findLlmsTxt(projectDir: string): string | null;
|
|
16
|
+
private extractDynamicParams;
|
|
17
|
+
}
|