@seo-partner/adapters 1.0.0 → 1.0.4
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/dist/ast-merger.d.ts +24 -0
- package/dist/ast-merger.d.ts.map +1 -0
- package/dist/ast-merger.js +85 -0
- package/dist/ast-merger.js.map +1 -0
- package/dist/astro-adapter.d.ts +6 -0
- package/dist/astro-adapter.d.ts.map +1 -1
- package/dist/astro-adapter.js +19 -0
- package/dist/astro-adapter.js.map +1 -1
- package/dist/blog-scaffold-generator.d.ts +8 -0
- package/dist/blog-scaffold-generator.d.ts.map +1 -0
- package/dist/blog-scaffold-generator.js +138 -0
- package/dist/blog-scaffold-generator.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/laravel-adapter.d.ts +7 -0
- package/dist/laravel-adapter.d.ts.map +1 -0
- package/dist/laravel-adapter.js +35 -0
- package/dist/laravel-adapter.js.map +1 -0
- package/dist/layout-title-resolver.d.ts +8 -0
- package/dist/layout-title-resolver.d.ts.map +1 -0
- package/dist/layout-title-resolver.js +28 -0
- package/dist/layout-title-resolver.js.map +1 -0
- package/dist/nuxt-adapter.d.ts +6 -0
- package/dist/nuxt-adapter.d.ts.map +1 -1
- package/dist/nuxt-adapter.js +20 -0
- package/dist/nuxt-adapter.js.map +1 -1
- package/dist/php-native-adapter.d.ts +7 -0
- package/dist/php-native-adapter.d.ts.map +1 -0
- package/dist/php-native-adapter.js +31 -0
- package/dist/php-native-adapter.js.map +1 -0
- package/dist/sitemap-builder.d.ts +10 -0
- package/dist/sitemap-builder.d.ts.map +1 -0
- package/dist/sitemap-builder.js +37 -0
- package/dist/sitemap-builder.js.map +1 -0
- package/dist/skeleton-detector.d.ts +14 -0
- package/dist/skeleton-detector.d.ts.map +1 -0
- package/dist/skeleton-detector.js +33 -0
- package/dist/skeleton-detector.js.map +1 -0
- package/dist/sveltekit-adapter.d.ts +6 -0
- package/dist/sveltekit-adapter.d.ts.map +1 -1
- package/dist/sveltekit-adapter.js +26 -0
- package/dist/sveltekit-adapter.js.map +1 -1
- package/dist/symfony-adapter.d.ts +7 -0
- package/dist/symfony-adapter.d.ts.map +1 -0
- package/dist/symfony-adapter.js +23 -0
- package/dist/symfony-adapter.js.map +1 -0
- package/package.json +2 -2
- package/src/ast-merger.ts +123 -0
- package/src/astro-adapter.ts +155 -126
- package/src/blog-scaffold-generator.ts +153 -0
- package/src/index.ts +8 -0
- package/src/laravel-adapter.ts +45 -0
- package/src/layout-title-resolver.ts +37 -0
- package/src/nuxt-adapter.ts +148 -118
- package/src/php-native-adapter.ts +46 -0
- package/src/sitemap-builder.ts +57 -0
- package/src/skeleton-detector.ts +56 -0
- package/src/sveltekit-adapter.ts +160 -119
- package/src/symfony-adapter.ts +33 -0
package/src/sveltekit-adapter.ts
CHANGED
|
@@ -1,119 +1,160 @@
|
|
|
1
|
-
import type { DiscoveredRoute, ExtractedRouteData } from '@seo-partner/core';
|
|
2
|
-
import type { AdapterCapabilities, AdapterVersion, FrameworkAdapter } from './contract.js';
|
|
3
|
-
import { parseSvelteComponent } from './parsers/svelte-parser.js';
|
|
4
|
-
import { normalizeRoutePattern } from './parsers/route-normalizer.js';
|
|
5
|
-
import { LayoutResolver } from './parsers/layout-resolver.js';
|
|
6
|
-
|
|
7
|
-
export class SvelteKitAdapter implements FrameworkAdapter {
|
|
8
|
-
public readonly id = 'sveltekit';
|
|
9
|
-
public readonly name = 'SvelteKit';
|
|
10
|
-
public readonly version: AdapterVersion = '1.0';
|
|
11
|
-
public readonly capabilities: AdapterCapabilities = {
|
|
12
|
-
routes: true,
|
|
13
|
-
metadata: true,
|
|
14
|
-
headings: true,
|
|
15
|
-
internalLinks: true,
|
|
16
|
-
jsonLd: true,
|
|
17
|
-
images: true,
|
|
18
|
-
robotsAndSitemap: true,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
private readonly layoutResolver = new LayoutResolver();
|
|
22
|
-
|
|
23
|
-
public async detect(
|
|
24
|
-
projectRoot: string,
|
|
25
|
-
filePaths: ReadonlyArray<string>,
|
|
26
|
-
contentsByPath: ReadonlyMap<string, string>,
|
|
27
|
-
): Promise<boolean> {
|
|
28
|
-
for (const filePath of filePaths) {
|
|
29
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
30
|
-
if (
|
|
31
|
-
normalized === 'svelte.config.js' ||
|
|
32
|
-
normalized === 'svelte.config.ts' ||
|
|
33
|
-
normalized === 'svelte.config.mjs' ||
|
|
34
|
-
normalized === 'svelte.config.cjs'
|
|
35
|
-
) {
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
if (normalized === 'package.json') {
|
|
39
|
-
const content = contentsByPath.get(filePath) ?? '';
|
|
40
|
-
if (content.includes('"@sveltejs/kit"')) {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
public async discoverRoutes(
|
|
49
|
-
projectRoot: string,
|
|
50
|
-
filePaths: ReadonlyArray<string>,
|
|
51
|
-
contentsByPath: ReadonlyMap<string, string>,
|
|
52
|
-
sitemapPaths: ReadonlySet<string>,
|
|
53
|
-
): Promise<ReadonlyArray<DiscoveredRoute>> {
|
|
54
|
-
const routes: DiscoveredRoute[] = [];
|
|
55
|
-
|
|
56
|
-
// Pre-cache layouts from src/routes/**/+layout.svelte
|
|
57
|
-
for (const filePath of filePaths) {
|
|
58
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
59
|
-
if (normalized.startsWith('src/routes/') && normalized.endsWith('+layout.svelte')) {
|
|
60
|
-
const content = contentsByPath.get(filePath) ?? '';
|
|
61
|
-
const parsed = parseSvelteComponent(
|
|
62
|
-
{
|
|
63
|
-
pattern: '',
|
|
64
|
-
filePath,
|
|
65
|
-
routerKind: 'sveltekit',
|
|
66
|
-
renderingMode: 'ssr-ssg',
|
|
67
|
-
isPrivate: false,
|
|
68
|
-
inSitemap: false,
|
|
69
|
-
},
|
|
70
|
-
content,
|
|
71
|
-
);
|
|
72
|
-
this.layoutResolver.registerLayout({
|
|
73
|
-
filePath,
|
|
74
|
-
htmlLang: parsed.htmlLang,
|
|
75
|
-
title: parsed.title,
|
|
76
|
-
description: parsed.description,
|
|
77
|
-
canonical: parsed.canonical,
|
|
78
|
-
robots: parsed.robots,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const filePath of filePaths) {
|
|
84
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
85
|
-
if (!normalized.startsWith('src/routes/')) continue;
|
|
86
|
-
if (!normalized.endsWith('+page.svelte')) continue;
|
|
87
|
-
|
|
88
|
-
const relativeInsideRoutes = normalized.slice('src/routes/'.length);
|
|
89
|
-
const pattern = normalizeRoutePattern(relativeInsideRoutes, 'sveltekit');
|
|
90
|
-
const inSitemap = sitemapPaths.has(pattern);
|
|
91
|
-
|
|
92
|
-
routes.push({
|
|
93
|
-
pattern,
|
|
94
|
-
filePath,
|
|
95
|
-
routerKind: 'sveltekit',
|
|
96
|
-
renderingMode: 'ssr-ssg',
|
|
97
|
-
isPrivate: false,
|
|
98
|
-
inSitemap,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return routes;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
public async extractRoute(
|
|
106
|
-
projectRoot: string,
|
|
107
|
-
route: DiscoveredRoute,
|
|
108
|
-
fileContent: string,
|
|
109
|
-
): Promise<ExtractedRouteData> {
|
|
110
|
-
let extracted = parseSvelteComponent(route, fileContent);
|
|
111
|
-
|
|
112
|
-
// Apply hierarchical layout inheritance from leaf up to root
|
|
113
|
-
const parentLayouts = this.layoutResolver.findSvelteKitLayoutPaths(route.filePath);
|
|
114
|
-
extracted = this.layoutResolver.applyInheritance(extracted, parentLayouts);
|
|
115
|
-
|
|
116
|
-
return extracted;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
1
|
+
import type { DiscoveredRoute, ExtractedRouteData } from '@seo-partner/core';
|
|
2
|
+
import type { AdapterCapabilities, AdapterVersion, FrameworkAdapter } from './contract.js';
|
|
3
|
+
import { parseSvelteComponent } from './parsers/svelte-parser.js';
|
|
4
|
+
import { normalizeRoutePattern } from './parsers/route-normalizer.js';
|
|
5
|
+
import { LayoutResolver } from './parsers/layout-resolver.js';
|
|
6
|
+
|
|
7
|
+
export class SvelteKitAdapter implements FrameworkAdapter {
|
|
8
|
+
public readonly id = 'sveltekit';
|
|
9
|
+
public readonly name = 'SvelteKit';
|
|
10
|
+
public readonly version: AdapterVersion = '1.0';
|
|
11
|
+
public readonly capabilities: AdapterCapabilities = {
|
|
12
|
+
routes: true,
|
|
13
|
+
metadata: true,
|
|
14
|
+
headings: true,
|
|
15
|
+
internalLinks: true,
|
|
16
|
+
jsonLd: true,
|
|
17
|
+
images: true,
|
|
18
|
+
robotsAndSitemap: true,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
private readonly layoutResolver = new LayoutResolver();
|
|
22
|
+
|
|
23
|
+
public async detect(
|
|
24
|
+
projectRoot: string,
|
|
25
|
+
filePaths: ReadonlyArray<string>,
|
|
26
|
+
contentsByPath: ReadonlyMap<string, string>,
|
|
27
|
+
): Promise<boolean> {
|
|
28
|
+
for (const filePath of filePaths) {
|
|
29
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
30
|
+
if (
|
|
31
|
+
normalized === 'svelte.config.js' ||
|
|
32
|
+
normalized === 'svelte.config.ts' ||
|
|
33
|
+
normalized === 'svelte.config.mjs' ||
|
|
34
|
+
normalized === 'svelte.config.cjs'
|
|
35
|
+
) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (normalized === 'package.json') {
|
|
39
|
+
const content = contentsByPath.get(filePath) ?? '';
|
|
40
|
+
if (content.includes('"@sveltejs/kit"')) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public async discoverRoutes(
|
|
49
|
+
projectRoot: string,
|
|
50
|
+
filePaths: ReadonlyArray<string>,
|
|
51
|
+
contentsByPath: ReadonlyMap<string, string>,
|
|
52
|
+
sitemapPaths: ReadonlySet<string>,
|
|
53
|
+
): Promise<ReadonlyArray<DiscoveredRoute>> {
|
|
54
|
+
const routes: DiscoveredRoute[] = [];
|
|
55
|
+
|
|
56
|
+
// Pre-cache layouts from src/routes/**/+layout.svelte
|
|
57
|
+
for (const filePath of filePaths) {
|
|
58
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
59
|
+
if (normalized.startsWith('src/routes/') && normalized.endsWith('+layout.svelte')) {
|
|
60
|
+
const content = contentsByPath.get(filePath) ?? '';
|
|
61
|
+
const parsed = parseSvelteComponent(
|
|
62
|
+
{
|
|
63
|
+
pattern: '',
|
|
64
|
+
filePath,
|
|
65
|
+
routerKind: 'sveltekit',
|
|
66
|
+
renderingMode: 'ssr-ssg',
|
|
67
|
+
isPrivate: false,
|
|
68
|
+
inSitemap: false,
|
|
69
|
+
},
|
|
70
|
+
content,
|
|
71
|
+
);
|
|
72
|
+
this.layoutResolver.registerLayout({
|
|
73
|
+
filePath,
|
|
74
|
+
htmlLang: parsed.htmlLang,
|
|
75
|
+
title: parsed.title,
|
|
76
|
+
description: parsed.description,
|
|
77
|
+
canonical: parsed.canonical,
|
|
78
|
+
robots: parsed.robots,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const filePath of filePaths) {
|
|
84
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
85
|
+
if (!normalized.startsWith('src/routes/')) continue;
|
|
86
|
+
if (!normalized.endsWith('+page.svelte')) continue;
|
|
87
|
+
|
|
88
|
+
const relativeInsideRoutes = normalized.slice('src/routes/'.length);
|
|
89
|
+
const pattern = normalizeRoutePattern(relativeInsideRoutes, 'sveltekit');
|
|
90
|
+
const inSitemap = sitemapPaths.has(pattern);
|
|
91
|
+
|
|
92
|
+
routes.push({
|
|
93
|
+
pattern,
|
|
94
|
+
filePath,
|
|
95
|
+
routerKind: 'sveltekit',
|
|
96
|
+
renderingMode: 'ssr-ssg',
|
|
97
|
+
isPrivate: false,
|
|
98
|
+
inSitemap,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return routes;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public async extractRoute(
|
|
106
|
+
projectRoot: string,
|
|
107
|
+
route: DiscoveredRoute,
|
|
108
|
+
fileContent: string,
|
|
109
|
+
): Promise<ExtractedRouteData> {
|
|
110
|
+
let extracted = parseSvelteComponent(route, fileContent);
|
|
111
|
+
|
|
112
|
+
// Apply hierarchical layout inheritance from leaf up to root
|
|
113
|
+
const parentLayouts = this.layoutResolver.findSvelteKitLayoutPaths(route.filePath);
|
|
114
|
+
extracted = this.layoutResolver.applyInheritance(extracted, parentLayouts);
|
|
115
|
+
|
|
116
|
+
return extracted;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
export interface SvelteKitMetaFields {
|
|
122
|
+
title?: string;
|
|
123
|
+
description?: string;
|
|
124
|
+
canonical?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function patchSvelteKitTemplate(
|
|
128
|
+
sourceCode: string,
|
|
129
|
+
meta: SvelteKitMetaFields,
|
|
130
|
+
): string {
|
|
131
|
+
let result = sourceCode;
|
|
132
|
+
|
|
133
|
+
if (/<svelte:head>/i.test(result)) {
|
|
134
|
+
if (meta.title && !/<title>/i.test(result)) {
|
|
135
|
+
result = result.replace(/(<svelte:head>\s*\n)/i, `$1 <title>${meta.title}</title>\n`);
|
|
136
|
+
}
|
|
137
|
+
if (meta.description && !/<meta\s+name=["']description["']/i.test(result)) {
|
|
138
|
+
result = result.replace(
|
|
139
|
+
/(<svelte:head>\s*\n)/i,
|
|
140
|
+
`$1 <meta name="description" content="${meta.description.replace(/"/g, '"')}" />\n`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
const headLines: string[] = ['<svelte:head>'];
|
|
145
|
+
if (meta.title) {
|
|
146
|
+
headLines.push(` <title>${meta.title}</title>`);
|
|
147
|
+
}
|
|
148
|
+
if (meta.description) {
|
|
149
|
+
headLines.push(` <meta name="description" content="${meta.description.replace(/"/g, '"')}" />`);
|
|
150
|
+
}
|
|
151
|
+
if (meta.canonical) {
|
|
152
|
+
headLines.push(` <link rel="canonical" href="${meta.canonical}" />`);
|
|
153
|
+
}
|
|
154
|
+
headLines.push('</svelte:head>\n\n');
|
|
155
|
+
|
|
156
|
+
result = headLines.join('\n') + result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface SymfonyMetaFields {
|
|
2
|
+
title?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
canonical?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function patchSymfonyTwigTemplate(
|
|
8
|
+
sourceCode: string,
|
|
9
|
+
meta: SymfonyMetaFields,
|
|
10
|
+
): string {
|
|
11
|
+
let result = sourceCode;
|
|
12
|
+
|
|
13
|
+
if (meta.title && !/{%\s*block\s+title\s*%}/i.test(result)) {
|
|
14
|
+
const titleBlock = `{% block title %}${meta.title}{% endblock %}\n`;
|
|
15
|
+
if (/{%\s*extends[^%]+%}/i.test(result)) {
|
|
16
|
+
result = result.replace(/({%\s*extends[^%]+%}\s*\n)/i, `$1${titleBlock}`);
|
|
17
|
+
} else {
|
|
18
|
+
result = titleBlock + result;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (meta.description && !/<meta\s+name=["']description["']/i.test(result)) {
|
|
23
|
+
const descTag = `<meta name="description" content="${meta.description.replace(/"/g, '"')}">\n`;
|
|
24
|
+
if (/{%\s*block\s+title\s*%}[\s\S]*?{%\s*endblock\s*%}/i.test(result)) {
|
|
25
|
+
result = result.replace(/({%\s*block\s+title\s*%}[\s\S]*?{%\s*endblock\s*%}\s*\n)/i, `$1${descTag}`);
|
|
26
|
+
} else {
|
|
27
|
+
result = descTag + result;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|