aeoptimize 0.6.2 → 0.7.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.
@@ -0,0 +1,301 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { extname } from 'node:path';
3
+ import { fetchUrlResource, parseHtml, parseMarkdown } from './scanner.js';
4
+ const AUDIT_CONTRACT_VERSION = '1.0';
5
+ const MAX_FILE_SIZE = 5 * 1024 * 1024;
6
+ function evidence(source, observed) {
7
+ return { source, observed };
8
+ }
9
+ function sourceFor(context) {
10
+ return context.rendering === 'markdown' ? 'markdown' : 'html';
11
+ }
12
+ function makeCheck(id, label, status, checkEvidence, explanation, remediation, validation) {
13
+ return {
14
+ id,
15
+ label,
16
+ status,
17
+ evidence: checkEvidence,
18
+ explanation,
19
+ remediation,
20
+ validation,
21
+ };
22
+ }
23
+ function checkHttp(context) {
24
+ if (!context.http) {
25
+ return makeCheck('http-status', 'HTTP status and redirects', 'N/A', [evidence('derived', { reason: 'Local files have no HTTP response.' })], 'No network request was made for this local document.', null, 'Serve the built page over HTTP and audit its public or staging URL.');
26
+ }
27
+ const { status, statusText, redirects, contentType } = context.http;
28
+ const statusResult = status >= 200 && status < 300 ? 'PASS' : 'FAIL';
29
+ const redirectChain = redirects.map((hop) => `${hop.status} ${hop.from} -> ${hop.to}`);
30
+ return makeCheck('http-status', 'HTTP status and redirects', statusResult, [evidence('http', {
31
+ status,
32
+ statusText,
33
+ finalUrl: context.target.finalUrl ?? context.target.input,
34
+ redirectCount: redirects.length,
35
+ redirectChain,
36
+ contentType,
37
+ })], statusResult === 'PASS'
38
+ ? `The final response returned HTTP ${status}. Redirects are reported as observed facts and are not scored.`
39
+ : `The final response returned HTTP ${status}, so the page was not retrieved as a successful document.`, statusResult === 'PASS'
40
+ ? null
41
+ : 'Restore a successful final response or intentionally return the correct removal status, then update links and sitemaps that still reference this URL.', 'Request the URL again with redirects disabled and confirm every hop plus the final status.');
42
+ }
43
+ function checkTitle(doc, context) {
44
+ const observedTitle = context.rendering === 'markdown' ? doc.title : (doc.documentTitle ?? '');
45
+ const status = observedTitle.trim() ? 'PASS' : 'FAIL';
46
+ return makeCheck('document-title', 'Document title', status, [evidence(sourceFor(context), { title: observedTitle || null })], status === 'PASS'
47
+ ? 'A document title was found. This check does not impose a fixed character count.'
48
+ : 'No document title was found in the HTML title element or Markdown title source.', status === 'PASS'
49
+ ? null
50
+ : 'Add a concise, page-specific title that accurately describes the visible content.', 'Inspect the rendered document title and verify that it remains page-specific after the build.');
51
+ }
52
+ function checkDescription(doc, context) {
53
+ const frontmatterDescription = typeof doc.frontmatter?.description === 'string'
54
+ ? doc.frontmatter.description
55
+ : '';
56
+ const description = doc.metaTags.description || frontmatterDescription;
57
+ if (!description && context.rendering === 'markdown') {
58
+ return makeCheck('meta-description', 'Meta description', 'N/A', [evidence('markdown', { description: null })], 'The source Markdown has no description field. Its final HTML template was not inspected.', null, 'Audit the rendered HTML and inspect its meta description.');
59
+ }
60
+ const status = description ? 'PASS' : 'WARNING';
61
+ return makeCheck('meta-description', 'Meta description', status, [evidence(sourceFor(context), { description: description || null })], status === 'PASS'
62
+ ? 'A page-specific description candidate is present. Search engines may still choose another snippet.'
63
+ : 'No meta description was found. This is a review item, not proof of a search-performance problem.', status === 'PASS'
64
+ ? null
65
+ : 'Add an accurate page-specific description when the template should provide one. Do not pad it to a fixed length.', 'Inspect the built HTML and compare the description with the visible page purpose.');
66
+ }
67
+ function checkHeadings(doc, context) {
68
+ const h1Count = doc.headings.filter((heading) => heading.level === 1).length;
69
+ const skippedLevels = [];
70
+ for (let index = 1; index < doc.headings.length; index += 1) {
71
+ const previous = doc.headings[index - 1].level;
72
+ const current = doc.headings[index].level;
73
+ if (current > previous + 1)
74
+ skippedLevels.push(`H${previous} -> H${current}`);
75
+ }
76
+ let status = 'PASS';
77
+ let explanation = 'The document exposes a main heading and no skipped heading levels were detected.';
78
+ let remediation = null;
79
+ if (doc.headings.length === 0) {
80
+ status = 'FAIL';
81
+ explanation = 'No headings were found in the inspected document.';
82
+ remediation = 'Add descriptive headings that reflect the document hierarchy and visible content.';
83
+ }
84
+ else if (h1Count === 0 || skippedLevels.length > 0) {
85
+ status = 'WARNING';
86
+ explanation = 'The heading outline needs review. Multiple H1 elements are reported as evidence but are not an automatic failure.';
87
+ remediation = 'Review the primary heading and skipped levels, then adjust only where the visual and semantic hierarchy is unclear.';
88
+ }
89
+ return makeCheck('heading-structure', 'Heading structure', status, [evidence(sourceFor(context), {
90
+ headingCount: doc.headings.length,
91
+ h1Count,
92
+ skippedLevels,
93
+ outline: doc.headings.slice(0, 20).map((heading) => `H${heading.level}: ${heading.text}`),
94
+ })], explanation, remediation, 'Inspect the rendered outline and verify that headings match the visible section hierarchy.');
95
+ }
96
+ function checkLanguage(doc, context) {
97
+ if (!doc.language && context.rendering === 'markdown') {
98
+ return makeCheck('document-language', 'Document language', 'N/A', [evidence('markdown', { language: null })], 'No language field was found in Markdown frontmatter, and the rendered HTML was not inspected.', null, 'Audit the rendered HTML and compare its html lang value with the visible language.');
99
+ }
100
+ const status = doc.language ? 'PASS' : 'WARNING';
101
+ return makeCheck('document-language', 'Document language', status, [evidence(sourceFor(context), { language: doc.language ?? null })], status === 'PASS'
102
+ ? 'An html lang or Markdown language value was found. Language-content agreement still needs human review.'
103
+ : 'The HTML document does not declare a language.', status === 'PASS' ? null : 'Add the appropriate html lang value to the document template.', 'Compare the declared language with the primary visible language and test representative localized pages.');
104
+ }
105
+ function resolveCanonical(value, baseUrl) {
106
+ try {
107
+ const url = baseUrl ? new URL(value, baseUrl) : new URL(value);
108
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
109
+ return null;
110
+ return url.toString();
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ function withoutHash(value) {
117
+ const url = new URL(value);
118
+ url.hash = '';
119
+ return url.toString();
120
+ }
121
+ function checkCanonical(doc, context) {
122
+ const canonicals = doc.canonicalLinks ?? [];
123
+ if (canonicals.length === 0) {
124
+ const isMarkdown = context.rendering === 'markdown';
125
+ return makeCheck('canonical-link', 'Canonical link', isMarkdown ? 'N/A' : 'WARNING', [evidence(sourceFor(context), { canonicals: [] })], isMarkdown
126
+ ? 'No canonical field was found in Markdown frontmatter, and the rendered template was not inspected.'
127
+ : 'No canonical link was found. Canonical markup is context-dependent, so absence is a review item.', isMarkdown ? null : 'Add one canonical link when the page participates in a duplicate or URL-normalization strategy.', 'Inspect the final HTML, resolve the canonical URL, and confirm that it matches the intended indexable URL.');
128
+ }
129
+ const baseUrl = context.target.finalUrl;
130
+ if (canonicals.length > 1) {
131
+ return makeCheck('canonical-link', 'Canonical link', 'FAIL', [evidence(sourceFor(context), {
132
+ canonicals,
133
+ resolved: canonicals.map((value) => resolveCanonical(value, baseUrl) ?? 'INVALID'),
134
+ })], 'Multiple canonical links were found, so the canonical signal is ambiguous.', 'Emit exactly one valid canonical link that represents the intended indexable URL.', 'Fetch the built page and verify one canonical element plus its resolved destination.');
135
+ }
136
+ const declared = canonicals[0];
137
+ const canonical = resolveCanonical(declared, baseUrl);
138
+ if (!baseUrl && declared.trim() !== '' && canonical === null && !/^[a-z][a-z\d+.-]*:/i.test(declared)) {
139
+ return makeCheck('canonical-link', 'Canonical link', 'N/A', [evidence(sourceFor(context), { declared, resolved: null, finalUrl: null })], 'A relative canonical was found, but a local-file audit has no deployed base URL for resolution.', null, 'Audit the deployed URL and verify the resolved canonical destination.');
140
+ }
141
+ if (canonical === null) {
142
+ return makeCheck('canonical-link', 'Canonical link', 'FAIL', [evidence(sourceFor(context), { declared, resolved: 'INVALID' })], 'The canonical value could not be resolved to an HTTP or HTTPS URL.', 'Emit exactly one valid canonical link that represents the intended indexable URL.', 'Fetch the built page and verify one canonical element plus its resolved destination.');
143
+ }
144
+ if (!baseUrl) {
145
+ return makeCheck('canonical-link', 'Canonical link', 'WARNING', [evidence(sourceFor(context), { declared: canonicals[0], resolved: canonical, finalUrl: null })], 'One valid canonical link was found, but a local-file audit cannot compare it with the deployed final URL.', 'Confirm that the deployed page resolves to the intended canonical URL.', 'Audit the deployed URL and compare its final response URL with the rendered canonical element.');
146
+ }
147
+ const differsFromFinal = baseUrl ? withoutHash(canonical) !== withoutHash(baseUrl) : false;
148
+ return makeCheck('canonical-link', 'Canonical link', differsFromFinal ? 'WARNING' : 'PASS', [evidence(sourceFor(context), { declared: canonicals[0], resolved: canonical, finalUrl: baseUrl ?? null })], differsFromFinal
149
+ ? 'The canonical resolves to a different URL than the final response. This can be intentional and needs review.'
150
+ : 'One valid canonical link was found and it resolves to the inspected final URL.', differsFromFinal ? 'Confirm the intended primary URL and update the canonical or serving URL if they conflict.' : null, 'Request both URLs, confirm their final status, and inspect the rendered canonical element.');
151
+ }
152
+ function checkRobots(doc, context) {
153
+ const valuesFor = (name) => doc.metaTagValues?.[name]
154
+ ?? (doc.metaTags[name] ? [doc.metaTags[name]] : []);
155
+ const metaRobots = [...valuesFor('robots'), ...valuesFor('googlebot')].join(', ');
156
+ const xRobotsTag = context.http?.xRobotsTag ?? '';
157
+ const combined = `${metaRobots}, ${xRobotsTag}`;
158
+ const blocksIndexing = /(?:^|[,\s])(?:noindex|none)(?:$|[,\s])/i.test(combined);
159
+ const blocksFollowing = /(?:^|[,\s])nofollow(?:$|[,\s])/i.test(combined);
160
+ const status = blocksIndexing || blocksFollowing ? 'WARNING' : 'PASS';
161
+ return makeCheck('robots-directives', 'Page-level robots directives', status, [
162
+ evidence(sourceFor(context), { metaRobots: metaRobots || null }),
163
+ context.http
164
+ ? evidence('http', { xRobotsTag: xRobotsTag || null })
165
+ : evidence('derived', { xRobotsTag: null, reason: 'Local files have no HTTP response headers.' }),
166
+ ], status === 'PASS'
167
+ ? 'No page-level noindex or nofollow directive was detected. robots.txt, authentication, CDN policy, and actual index state were not checked.'
168
+ : 'A page-level indexing or following restriction was detected. Its intent cannot be inferred automatically.', status === 'PASS'
169
+ ? null
170
+ : 'Confirm whether the directive is intentional. Remove or narrow it only when the page should be indexable or its links should be followed.', 'Inspect both the response headers and rendered meta robots tags, then verify the intended policy with the responsible owner.');
171
+ }
172
+ function checkLinks(doc, context) {
173
+ const links = doc.links;
174
+ const emptyText = links.filter((link) => !link.text.trim()).length;
175
+ const scriptLinks = links.filter((link) => /^javascript:/i.test(link.href)).length;
176
+ const status = links.length === 0 || emptyText > 0 || scriptLinks > 0 ? 'WARNING' : 'PASS';
177
+ return makeCheck('link-discovery', 'Link discovery', status, [evidence(sourceFor(context), {
178
+ linkCount: links.length,
179
+ emptyTextCount: emptyText,
180
+ javascriptLinkCount: scriptLinks,
181
+ sample: links.slice(0, 20).map((link) => `${link.text || '[empty]'} -> ${link.href}`),
182
+ })], links.length === 0
183
+ ? 'No links were discovered in the inspected document.'
184
+ : 'Links were extracted from the document. Their destination status and site-graph role were not fetched in this single-page audit.', status === 'PASS'
185
+ ? null
186
+ : 'Review missing link text and script-only navigation. Add crawlable links when they match the intended navigation and content relationships.', 'Fetch link destinations separately and compare the rendered navigation with the extracted link sample.');
187
+ }
188
+ function checkImages(doc, context) {
189
+ const images = doc.images ?? [];
190
+ if (images.length === 0) {
191
+ return makeCheck('image-alternatives', 'Image alternative text', 'N/A', [evidence(sourceFor(context), { imageCount: 0 })], 'No images were found in the inspected document.', null, 'Inspect representative rendered templates if images are inserted after build or hydration.');
192
+ }
193
+ const missingAlt = images.filter((image) => image.alt === undefined).length;
194
+ const emptyAlt = images.filter((image) => image.alt === '').length;
195
+ const status = missingAlt > 0 ? 'WARNING' : 'PASS';
196
+ return makeCheck('image-alternatives', 'Image alternative text', status, [evidence(sourceFor(context), {
197
+ imageCount: images.length,
198
+ missingAltAttributeCount: missingAlt,
199
+ emptyAltCount: emptyAlt,
200
+ sample: images.slice(0, 20).map((image) => `${image.src || '[missing src]'} | alt=${image.alt ?? '[missing]'}`),
201
+ })], status === 'PASS'
202
+ ? 'Every discovered image has an alt attribute. Empty alt values can be correct for decorative images and need contextual review.'
203
+ : 'One or more images lack an alt attribute. The audit does not infer the correct description from pixels.', status === 'PASS'
204
+ ? null
205
+ : 'Add accurate alt text for informative images and an explicit empty alt value for images that are purely decorative.', 'Inspect each image in context with assistive-technology semantics and verify the built HTML attributes.');
206
+ }
207
+ function checkJsonLd(doc, context) {
208
+ const errors = doc.jsonLdErrors ?? [];
209
+ const parsedObjects = doc.jsonLd.filter((value) => value !== null && typeof value === 'object' && !Array.isArray(value));
210
+ const invalidValueCount = doc.jsonLd.length - parsedObjects.length;
211
+ if (errors.length > 0 || invalidValueCount > 0) {
212
+ return makeCheck('json-ld-structure', 'JSON-LD structure', 'FAIL', [evidence(sourceFor(context), {
213
+ blockCount: doc.jsonLdBlockCount ?? doc.jsonLd.length,
214
+ parsedObjectCount: parsedObjects.length,
215
+ invalidValueCount,
216
+ parseErrors: errors,
217
+ })], 'Malformed JSON-LD or a non-object top-level value was detected. Feature-specific eligibility and visible-content agreement remain outside this structural check.', 'Correct the JSON syntax and top-level structure, then validate the specific schema type against the applicable primary documentation and visible content.', 'Parse every JSON-LD block again and run the relevant structured-data validator for the page type.');
218
+ }
219
+ if (doc.jsonLd.length === 0) {
220
+ return makeCheck('json-ld-structure', 'JSON-LD structure', 'N/A', [evidence(sourceFor(context), { blockCount: 0, schemaTypes: [] })], 'No JSON-LD was found. Structured data is optional and absence is not treated as an error.', null, 'Add structured data only for a defined feature or entity need, then validate it against visible content.');
221
+ }
222
+ return makeCheck('json-ld-structure', 'JSON-LD structure', 'PASS', [evidence(sourceFor(context), {
223
+ blockCount: doc.jsonLdBlockCount ?? doc.jsonLd.length,
224
+ schemaTypes: parsedObjects.map((value) => value['@type'] || '[unknown]'),
225
+ })], 'Every JSON-LD value parsed to an object. Context, type, feature eligibility, and visible-content agreement require schema-specific validation.', null, 'Compare each structured-data field with visible content and the current documentation for its intended feature.');
226
+ }
227
+ export function auditDocument(doc, context) {
228
+ const checks = [
229
+ checkHttp(context),
230
+ checkTitle(doc, context),
231
+ checkDescription(doc, context),
232
+ checkHeadings(doc, context),
233
+ checkLanguage(doc, context),
234
+ checkCanonical(doc, context),
235
+ checkRobots(doc, context),
236
+ checkLinks(doc, context),
237
+ checkImages(doc, context),
238
+ checkJsonLd(doc, context),
239
+ ];
240
+ const summary = { PASS: 0, WARNING: 0, FAIL: 0, 'N/A': 0 };
241
+ for (const check of checks)
242
+ summary[check.status] += 1;
243
+ return {
244
+ contractVersion: AUDIT_CONTRACT_VERSION,
245
+ target: context.target,
246
+ rendering: context.rendering,
247
+ checks,
248
+ summary,
249
+ limitations: [
250
+ 'This command reports deterministic observations from one document. It does not claim ranking, indexing, rich-result display, traffic, conversion, or citation outcomes.',
251
+ 'Destination status, robots.txt, sitemap membership, hreflang reciprocity, site-wide duplication, orphan pages, Core Web Vitals, and analytics require separate evidence.',
252
+ 'PASS means the bounded check found no issue in the inspected evidence. It is not a guarantee of search-engine behavior.',
253
+ ],
254
+ timestamp: new Date().toISOString(),
255
+ };
256
+ }
257
+ async function auditFile(filePath) {
258
+ const fileStats = await stat(filePath);
259
+ if (!fileStats.isFile())
260
+ throw new Error('Audit target must be an HTML, HTM, Markdown, or MDX file.');
261
+ if (fileStats.size > MAX_FILE_SIZE) {
262
+ throw new Error(`File too large (${(fileStats.size / 1024 / 1024).toFixed(1)}MB). Maximum is 5MB.`);
263
+ }
264
+ const content = await readFile(filePath, 'utf8');
265
+ const extension = extname(filePath).toLowerCase();
266
+ if (extension === '.html' || extension === '.htm') {
267
+ return auditDocument(parseHtml(content, filePath), {
268
+ target: { type: 'file', input: filePath },
269
+ rendering: 'local-html',
270
+ });
271
+ }
272
+ if (extension === '.md' || extension === '.mdx') {
273
+ return auditDocument(parseMarkdown(content, filePath), {
274
+ target: { type: 'file', input: filePath },
275
+ rendering: 'markdown',
276
+ });
277
+ }
278
+ throw new Error(`Unsupported file type: ${extension}`);
279
+ }
280
+ async function auditUrl(url) {
281
+ const resource = await fetchUrlResource(url);
282
+ const doc = parseHtml(resource.html, resource.finalUrl);
283
+ return auditDocument(doc, {
284
+ target: { type: 'url', input: resource.requestedUrl, finalUrl: resource.finalUrl },
285
+ rendering: resource.renderedWithBrowser ? 'browser' : 'response-html',
286
+ http: {
287
+ status: resource.status,
288
+ statusText: resource.statusText,
289
+ redirects: resource.redirects,
290
+ contentType: resource.contentType,
291
+ xRobotsTag: resource.xRobotsTag,
292
+ },
293
+ });
294
+ }
295
+ export async function audit(target) {
296
+ if (target.type === 'directory') {
297
+ throw new Error('Evidence-backed audit currently supports one URL or one HTML/Markdown file.');
298
+ }
299
+ return target.type === 'url' ? auditUrl(target.path) : auditFile(target.path);
300
+ }
301
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/core/audit.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,10 @@
1
1
  export * from './types.js';
2
2
  export * from './scanner.js';
3
+ export * from './audit.js';
4
+ export * from './site-audit.js';
3
5
  export * from './rules.js';
4
6
  export * from './generator.js';
5
7
  export * from './ai-prompt.js';
6
8
  export * from './external-scorers.js';
7
9
  export * from './merger.js';
10
+ export * from './static-audit.js';
@@ -1,8 +1,11 @@
1
1
  export * from './types.js';
2
2
  export * from './scanner.js';
3
+ export * from './audit.js';
4
+ export * from './site-audit.js';
3
5
  export * from './rules.js';
4
6
  export * from './generator.js';
5
7
  export * from './ai-prompt.js';
6
8
  export * from './external-scorers.js';
7
9
  export * from './merger.js';
10
+ export * from './static-audit.js';
8
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC"}
@@ -4,5 +4,27 @@ export declare function parseMarkdown(md: string, url: string): ParsedDocument;
4
4
  export declare function scanDocument(doc: ParsedDocument): PageAnalysis;
5
5
  export declare function scanFile(filePath: string): Promise<PageAnalysis>;
6
6
  export declare function scanDirectory(dirPath: string): Promise<ScanReport>;
7
+ export interface RedirectHop {
8
+ from: string;
9
+ to: string;
10
+ status: number;
11
+ }
12
+ export interface UrlResource {
13
+ requestedUrl: string;
14
+ finalUrl: string;
15
+ status: number;
16
+ statusText: string;
17
+ redirects: RedirectHop[];
18
+ contentType: string | null;
19
+ xRobotsTag: string | null;
20
+ html: string;
21
+ renderedWithBrowser: boolean;
22
+ }
23
+ export interface UrlFetchOptions {
24
+ render?: boolean;
25
+ init?: RequestInit;
26
+ allowedOrigin?: string;
27
+ }
28
+ export declare function fetchUrlResource(url: string, options?: UrlFetchOptions): Promise<UrlResource>;
7
29
  export declare function scanUrl(url: string): Promise<ScanReport>;
8
30
  export declare function scan(target: ScanTarget): Promise<ScanReport>;
@@ -9,7 +9,8 @@ import { allRules } from './rules.js';
9
9
  export function parseHtml(html, url) {
10
10
  const $ = cheerio.load(html);
11
11
  // Extract title
12
- const title = $('title').first().text().trim() || $('h1').first().text().trim() || url;
12
+ const documentTitle = $('title').first().text().trim();
13
+ const title = documentTitle || $('h1').first().text().trim() || url;
13
14
  // Extract headings
14
15
  const headings = [];
15
16
  $('h1, h2, h3, h4, h5, h6').each((_, el) => {
@@ -28,27 +29,47 @@ export function parseHtml(html, url) {
28
29
  });
29
30
  // Extract JSON-LD
30
31
  const jsonLd = [];
31
- $('script[type="application/ld+json"]').each((_, el) => {
32
+ const jsonLdErrors = [];
33
+ const jsonLdScripts = $('script[type="application/ld+json"]');
34
+ jsonLdScripts.each((index, el) => {
32
35
  try {
33
36
  const parsed = JSON.parse($(el).html() || '');
34
- if (Array.isArray(parsed)) {
35
- jsonLd.push(...parsed);
36
- }
37
- else {
38
- jsonLd.push(parsed);
37
+ const candidates = Array.isArray(parsed) ? parsed : [parsed];
38
+ for (const [candidateIndex, candidate] of candidates.entries()) {
39
+ if (candidate !== null) {
40
+ // Keep the v0.6 scoring input compatible. The audit contract records
41
+ // non-object values separately, but the score historically counted them.
42
+ jsonLd.push(candidate);
43
+ }
44
+ if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
45
+ continue;
46
+ }
47
+ else {
48
+ const location = Array.isArray(parsed) ? ` item ${candidateIndex + 1}` : '';
49
+ jsonLdErrors.push(`JSON-LD block ${index + 1}${location}: expected an object`);
50
+ }
39
51
  }
40
52
  }
41
- catch {
42
- // Ignore malformed JSON-LD
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : 'Unknown JSON parse error';
55
+ jsonLdErrors.push(`JSON-LD block ${index + 1}: ${message}`);
43
56
  }
44
57
  });
45
58
  // Extract meta tags
46
59
  const metaTags = {};
60
+ const metaTagValues = {};
47
61
  $('meta').each((_, el) => {
48
- const name = $(el).attr('name') || $(el).attr('property') || '';
62
+ const declaredName = $(el).attr('name') || '';
63
+ const name = declaredName || $(el).attr('property') || '';
49
64
  const content = $(el).attr('content') || '';
50
65
  if (name && content)
51
66
  metaTags[name] = content;
67
+ if (declaredName && content) {
68
+ const normalizedName = declaredName.trim().toLowerCase();
69
+ const values = metaTagValues[normalizedName] ?? [];
70
+ values.push(content);
71
+ metaTagValues[normalizedName] = values;
72
+ }
52
73
  });
53
74
  // Extract links
54
75
  const links = [];
@@ -59,18 +80,34 @@ export function parseHtml(html, url) {
59
80
  if (href)
60
81
  links.push({ href, text, rel });
61
82
  });
83
+ const images = $('img').map((_, el) => ({
84
+ src: $(el).attr('src') || '',
85
+ alt: $(el).attr('alt'),
86
+ width: $(el).attr('width'),
87
+ height: $(el).attr('height'),
88
+ loading: $(el).attr('loading'),
89
+ })).get();
90
+ const language = $('html').attr('lang')?.trim() || undefined;
91
+ const canonicalLinks = $('link[rel~="canonical"]').map((_, el) => $(el).attr('href') || '').get();
62
92
  // Extract raw text (content area preferred)
63
93
  const contentArea = $('main, article, [role="main"]').first();
64
94
  const rawText = (contentArea.length > 0 ? contentArea.text() : $('body').text()).replace(/\s+/g, ' ').trim();
65
95
  return {
66
96
  url,
67
97
  title,
98
+ documentTitle,
68
99
  html,
69
100
  headings,
70
101
  paragraphs,
71
102
  jsonLd,
103
+ jsonLdBlockCount: jsonLdScripts.length,
104
+ jsonLdErrors,
72
105
  metaTags,
106
+ metaTagValues,
73
107
  links,
108
+ images,
109
+ language,
110
+ canonicalLinks,
74
111
  rawText,
75
112
  };
76
113
  }
@@ -124,16 +161,36 @@ export function parseMarkdown(md, url) {
124
161
  .replace(/[#*_~`>]/g, '')
125
162
  .replace(/\s+/g, ' ')
126
163
  .trim();
164
+ const links = [];
165
+ const images = [];
166
+ for (const match of content.matchAll(/(!?)\[([^\]]*)\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g)) {
167
+ const [, imageMarker, text, href] = match;
168
+ if (imageMarker) {
169
+ images.push({ src: href, alt: text });
170
+ }
171
+ else {
172
+ links.push({ href, text });
173
+ }
174
+ }
175
+ const canonicalValue = frontmatter.canonical;
176
+ const canonicalLinks = typeof canonicalValue === 'string' ? [canonicalValue] : [];
177
+ const languageValue = frontmatter.lang ?? frontmatter.language;
178
+ const language = typeof languageValue === 'string' ? languageValue : undefined;
127
179
  return {
128
180
  url,
129
181
  title: finalTitle,
182
+ documentTitle: typeof frontmatter.title === 'string' ? frontmatter.title : undefined,
130
183
  markdown: md,
131
184
  frontmatter: frontmatter,
132
185
  headings,
133
186
  paragraphs,
134
187
  jsonLd: [],
135
188
  metaTags: {},
136
- links: [],
189
+ metaTagValues: {},
190
+ links,
191
+ images,
192
+ language,
193
+ canonicalLinks,
137
194
  rawText,
138
195
  };
139
196
  }
@@ -235,12 +292,13 @@ function isBlockedHostname(hostname) {
235
292
  const blocked = /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|::1$|localhost$)/i;
236
293
  return blocked.test(hostname);
237
294
  }
238
- async function fetchWithSafeRedirects(input, init, maxRedirects = 10) {
295
+ async function fetchWithSafeRedirects(input, init, maxRedirects = 10, allowedOrigin) {
239
296
  let currentUrl = typeof input === 'string' ? validateUrl(input) : validateUrl(input.toString());
297
+ const redirects = [];
240
298
  for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount += 1) {
241
299
  const response = await fetch(currentUrl, { ...init, redirect: 'manual' });
242
300
  if (response.status < 300 || response.status >= 400) {
243
- return response;
301
+ return { response, finalUrl: currentUrl.toString(), redirects };
244
302
  }
245
303
  if (redirectCount === maxRedirects) {
246
304
  throw new Error(`Too many redirects while fetching ${currentUrl.toString()}`);
@@ -249,7 +307,16 @@ async function fetchWithSafeRedirects(input, init, maxRedirects = 10) {
249
307
  if (!location) {
250
308
  throw new Error(`Redirect response from ${currentUrl.toString()} missing Location header`);
251
309
  }
252
- currentUrl = validateUrl(new URL(location, currentUrl).toString());
310
+ const nextUrl = validateUrl(new URL(location, currentUrl).toString());
311
+ if (allowedOrigin && nextUrl.origin !== allowedOrigin) {
312
+ throw new Error(`Redirect left the allowed origin: ${nextUrl.origin}`);
313
+ }
314
+ redirects.push({
315
+ from: currentUrl.toString(),
316
+ to: nextUrl.toString(),
317
+ status: response.status,
318
+ });
319
+ currentUrl = nextUrl;
253
320
  }
254
321
  throw new Error(`Too many redirects while fetching ${currentUrl.toString()}`);
255
322
  }
@@ -305,28 +372,41 @@ function isSpaLikely(html) {
305
372
  const scripts = (html.match(/<script/gi) || []).length;
306
373
  return contentTags < 10 && scripts > 3;
307
374
  }
308
- export async function scanUrl(url) {
375
+ export async function fetchUrlResource(url, options = {}) {
309
376
  validateUrl(url);
310
- const response = await fetchWithSafeRedirects(url);
311
- if (!response.ok) {
312
- throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
313
- }
377
+ const { response, finalUrl, redirects } = await fetchWithSafeRedirects(url, options.init, 10, options.allowedOrigin);
314
378
  let html = await response.text();
315
379
  let renderedWithBrowser = false;
316
- // If page looks like an SPA, try puppeteer for full rendering
317
- if (isSpaLikely(html)) {
318
- const result = await fetchWithPuppeteer(url);
380
+ if (options.render !== false && response.ok && isSpaLikely(html)) {
381
+ const result = await fetchWithPuppeteer(finalUrl);
319
382
  if (result.rendered) {
320
383
  html = result.html;
321
384
  renderedWithBrowser = true;
322
385
  }
323
- else {
324
- console.warn('[aeoptimize] This page appears to be a JavaScript-rendered SPA.');
325
- console.warn('[aeoptimize] Install Chrome/Chromium for accurate scoring of JS-rendered sites.');
326
- console.warn('[aeoptimize] Without a browser, scores may be lower than actual content quality.\n');
327
- }
328
386
  }
329
- const doc = parseHtml(html, url);
387
+ return {
388
+ requestedUrl: url,
389
+ finalUrl,
390
+ status: response.status,
391
+ statusText: response.statusText,
392
+ redirects,
393
+ contentType: response.headers.get('content-type'),
394
+ xRobotsTag: response.headers.get('x-robots-tag'),
395
+ html,
396
+ renderedWithBrowser,
397
+ };
398
+ }
399
+ export async function scanUrl(url) {
400
+ const resource = await fetchUrlResource(url);
401
+ if (resource.status < 200 || resource.status >= 300) {
402
+ throw new Error(`Failed to fetch ${url}: ${resource.status} ${resource.statusText}`);
403
+ }
404
+ if (isSpaLikely(resource.html) && !resource.renderedWithBrowser) {
405
+ console.warn('[aeoptimize] This page appears to be a JavaScript-rendered SPA.');
406
+ console.warn('[aeoptimize] Install Chrome/Chromium for accurate scoring of JS-rendered sites.');
407
+ console.warn('[aeoptimize] Without a browser, scores may be lower than actual content quality.\n');
408
+ }
409
+ const doc = parseHtml(resource.html, url);
330
410
  const analysis = scanDocument(doc);
331
411
  return {
332
412
  pages: [analysis],