Skip to content

Commit a1bd0f6

Browse files
authored
Merge pull request #6 from multivmlabs/fix/score-security-improvements
fix: improve aeochecker score alignment, security fixes, and schema generation
2 parents f171932 + 96c80d3 commit a1bd0f6

14 files changed

+232
-32
lines changed

src/core/ai-index.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const baseConfig: ResolvedAeoConfig = {
2828
manifest: true,
2929
sitemap: true,
3030
aiIndex: true,
31+
schema: true,
3132
},
3233
robots: { allow: ['/'], disallow: [], crawlDelay: 0, sitemap: '' },
3334
widget: {
@@ -39,6 +40,17 @@ const baseConfig: ResolvedAeoConfig = {
3940
showBadge: true,
4041
size: 'default' as const,
4142
},
43+
schema: {
44+
enabled: true,
45+
organization: { name: 'Test', url: 'https://example.com', logo: '', sameAs: [] },
46+
defaultType: 'WebPage',
47+
},
48+
og: {
49+
enabled: false,
50+
image: '',
51+
twitterHandle: '',
52+
type: 'website',
53+
},
4254
};
4355

4456
describe('generateAIIndex', () => {

src/core/audit.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ function auditContentStructure(config: ResolvedAeoConfig, issues: AuditIssue[],
156156

157157
/**
158158
* Category 3: Schema Presence (0-20)
159-
* Checks: schema enabled, organization info, sameAs, defaultType, schema.json generated
159+
* Checks: schema enabled, organization info, FAQ/HowTo patterns, Article/WebPage type
160160
*/
161161
function auditSchemaPresence(config: ResolvedAeoConfig, issues: AuditIssue[], suggestions: string[]): AuditCategory {
162162
const checks: AuditCategory['checks'] = [];
@@ -182,18 +182,21 @@ function auditSchemaPresence(config: ResolvedAeoConfig, issues: AuditIssue[], su
182182
suggestions.push('Add schema.organization.logo for richer search results and AI knowledge');
183183
}
184184

185-
// sameAs social profiles (4 pts)
186-
const hasSameAs = config.schema.organization.sameAs.length > 0;
187-
checks.push({ label: 'Social profiles linked (sameAs)', passed: hasSameAs, points: hasSameAs ? 4 : 0 });
188-
if (!hasSameAs) {
189-
issues.push({ category: 'Schema Presence', severity: 'warning', message: 'No social profiles (sameAs) — critical for GEO/E-E-A-T signals', fix: 'Add schema.organization.sameAs with social profile URLs' });
185+
// FAQPage or HowTo schema (4 pts) — matches aeochecker scoring
186+
const hasFaqOrHowTo = config.pages.some(p => {
187+
const content = p.content || '';
188+
return /^#{1,6}\s+.+\?\s*$/m.test(content) || /^#{1,6}\s+(?:Step\s+\d+[\s:.-]|How\s+to)/im.test(content);
189+
});
190+
checks.push({ label: 'FAQPage or HowTo schema', passed: hasFaqOrHowTo, points: hasFaqOrHowTo ? 4 : 0 });
191+
if (!hasFaqOrHowTo) {
192+
suggestions.push('Add FAQ sections (question headings) or step-by-step content to auto-generate FAQPage/HowTo schema');
190193
}
191194

192-
// URL is not default (4 pts)
193-
const hasRealUrl = config.url !== 'https://example.com' && config.url !== '';
194-
checks.push({ label: 'Site URL is configured (not default)', passed: hasRealUrl, points: hasRealUrl ? 4 : 0 });
195-
if (!hasRealUrl) {
196-
issues.push({ category: 'Schema Presence', severity: 'error', message: 'Site URL is still the default (https://example.com)', fix: 'Set url to your actual site URL' });
195+
// Article/WebPage schema (4 pts) — always passes when schema is enabled since defaultType is set
196+
const hasArticleOrWebPage = schemaEnabled && (config.schema.defaultType === 'Article' || config.schema.defaultType === 'WebPage');
197+
checks.push({ label: 'Article/WebPage schema', passed: hasArticleOrWebPage, points: hasArticleOrWebPage ? 4 : 0 });
198+
if (!hasArticleOrWebPage && schemaEnabled) {
199+
suggestions.push('Set schema.defaultType to "Article" or "WebPage" for per-page structured data');
197200
}
198201

199202
return {
@@ -247,13 +250,11 @@ function auditMetaQuality(config: ResolvedAeoConfig, issues: AuditIssue[], sugge
247250
issues.push({ category: 'Meta Quality', severity: 'warning', message: `Only ${pagesWithTitles.length}/${config.pages.length} pages have titles`, fix: 'Add titles to all pages' });
248251
}
249252

250-
// Pages have descriptions (4 pts)
251-
const pagesWithDesc = config.pages.filter(p => p.description && p.description.length > 0);
252-
const descCoverage = config.pages.length > 0 ? pagesWithDesc.length / config.pages.length : 0;
253-
const goodDescCoverage = descCoverage >= 0.5;
254-
checks.push({ label: '50%+ of pages have descriptions', passed: goodDescCoverage, points: goodDescCoverage ? 4 : 0 });
255-
if (!goodDescCoverage && config.pages.length > 0) {
256-
suggestions.push(`Only ${pagesWithDesc.length}/${config.pages.length} pages have descriptions — add per-page descriptions`);
253+
// OG image set (4 pts)
254+
const hasOgImage = !!config.og.image;
255+
checks.push({ label: 'OG image configured', passed: hasOgImage, points: hasOgImage ? 4 : 0 });
256+
if (!hasOgImage) {
257+
suggestions.push('Set og.image for richer social sharing previews and AI citation cards');
257258
}
258259

259260
return {

src/core/generate-wrapper.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as rawMarkdown from './raw-markdown';
77
import * as manifest from './manifest';
88
import * as sitemap from './sitemap';
99
import * as aiIndex from './ai-index';
10+
import * as schema from './schema';
1011
import type { ResolvedAeoConfig } from '../types';
1112

1213
vi.mock('fs', () => ({
@@ -34,6 +35,7 @@ const baseConfig: ResolvedAeoConfig = {
3435
manifest: true,
3536
sitemap: true,
3637
aiIndex: true,
38+
schema: true,
3739
},
3840
robots: { allow: ['/'], disallow: [], crawlDelay: 0, sitemap: '' },
3941
widget: {
@@ -45,6 +47,17 @@ const baseConfig: ResolvedAeoConfig = {
4547
showBadge: true,
4648
size: 'default' as const,
4749
},
50+
schema: {
51+
enabled: true,
52+
organization: { name: 'Test', url: 'https://example.com', logo: '', sameAs: [] },
53+
defaultType: 'WebPage',
54+
},
55+
og: {
56+
enabled: false,
57+
image: '',
58+
twitterHandle: '',
59+
type: 'website',
60+
},
4861
};
4962

5063
describe('generateAEOFiles', () => {
@@ -90,6 +103,7 @@ describe('generateAEOFiles', () => {
90103
manifest: true,
91104
sitemap: false,
92105
aiIndex: false,
106+
schema: false,
93107
},
94108
};
95109

@@ -129,6 +143,7 @@ describe('generateAEOFiles', () => {
129143
vi.spyOn(aiIndex, 'generateAIIndex').mockImplementation(() => { throw new Error('fail'); });
130144
vi.spyOn(rawMarkdown, 'generatePageMarkdownFiles').mockImplementation(() => { throw new Error('fail'); });
131145
vi.spyOn(rawMarkdown, 'copyMarkdownFiles').mockImplementation(() => { throw new Error('fail'); });
146+
vi.spyOn(schema, 'generateSchema').mockImplementation(() => { throw new Error('fail'); });
132147

133148
const result = await generateAEOFiles(baseConfig);
134149

src/core/llms-full.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const baseConfig: ResolvedAeoConfig = {
2424
manifest: true,
2525
sitemap: true,
2626
aiIndex: true,
27+
schema: true,
2728
},
2829
robots: { allow: ['/'], disallow: [], crawlDelay: 0, sitemap: '' },
2930
widget: {
@@ -35,6 +36,17 @@ const baseConfig: ResolvedAeoConfig = {
3536
showBadge: true,
3637
size: 'default' as const,
3738
},
39+
schema: {
40+
enabled: true,
41+
organization: { name: 'Test', url: 'https://example.com', logo: '', sameAs: [] },
42+
defaultType: 'WebPage',
43+
},
44+
og: {
45+
enabled: false,
46+
image: '',
47+
twitterHandle: '',
48+
type: 'website',
49+
},
3850
};
3951

4052
describe('generateLlmsFullTxt', () => {

src/core/llms-txt.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const baseConfig: ResolvedAeoConfig = {
2424
manifest: true,
2525
sitemap: true,
2626
aiIndex: true,
27+
schema: true,
2728
},
2829
robots: { allow: ['/'], disallow: [], crawlDelay: 0, sitemap: '' },
2930
widget: {
@@ -35,6 +36,17 @@ const baseConfig: ResolvedAeoConfig = {
3536
showBadge: true,
3637
size: 'default' as const,
3738
},
39+
schema: {
40+
enabled: true,
41+
organization: { name: 'Test', url: 'https://example.com', logo: '', sameAs: [] },
42+
defaultType: 'WebPage',
43+
},
44+
og: {
45+
enabled: false,
46+
image: '',
47+
twitterHandle: '',
48+
type: 'website',
49+
},
3850
};
3951

4052
describe('generateLlmsTxt', () => {

src/core/manifest.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const baseConfig: ResolvedAeoConfig = {
2929
manifest: true,
3030
sitemap: true,
3131
aiIndex: true,
32+
schema: true,
3233
},
3334
robots: { allow: ['/'], disallow: [], crawlDelay: 0, sitemap: '' },
3435
widget: {
@@ -40,6 +41,17 @@ const baseConfig: ResolvedAeoConfig = {
4041
showBadge: true,
4142
size: 'default' as const,
4243
},
44+
schema: {
45+
enabled: true,
46+
organization: { name: 'Test', url: 'https://example.com', logo: '', sameAs: [] },
47+
defaultType: 'WebPage',
48+
},
49+
og: {
50+
enabled: false,
51+
image: '',
52+
twitterHandle: '',
53+
type: 'website',
54+
},
4355
};
4456

4557
describe('generateManifest', () => {

src/core/opengraph.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function makeConfig(overrides: Partial<ResolvedAeoConfig> = {}): ResolvedAeoConf
1010
pages: [],
1111
outDir: './out',
1212
contentDir: '',
13-
generators: { robots: true, llmsTxt: true, llmsFullTxt: true, sitemap: true, aiIndex: true, docs: true, schema: true },
13+
generators: { robotsTxt: true, llmsTxt: true, llmsFullTxt: true, rawMarkdown: true, manifest: true, sitemap: true, aiIndex: true, schema: true },
1414
schema: {
1515
enabled: true,
1616
organization: { name: 'Test Org', url: 'https://example.com', logo: '', sameAs: [] },

src/core/raw-markdown.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const createConfig = (overrides = {}): ResolvedAeoConfig => ({
4646
manifest: true,
4747
sitemap: true,
4848
aiIndex: true,
49+
schema: true,
4950
},
5051
robots: { allow: ['/'], disallow: [], crawlDelay: 0, sitemap: '' },
5152
widget: {
@@ -57,6 +58,17 @@ const createConfig = (overrides = {}): ResolvedAeoConfig => ({
5758
showBadge: true,
5859
size: 'default' as const,
5960
},
61+
schema: {
62+
enabled: true,
63+
organization: { name: 'Test', url: 'https://example.com', logo: '', sameAs: [] },
64+
defaultType: 'WebPage',
65+
},
66+
og: {
67+
enabled: false,
68+
image: '',
69+
twitterHandle: '',
70+
type: 'website',
71+
},
6072
...overrides,
6173
});
6274

src/core/robots.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe('generateRobotsTxt', () => {
1818
manifest: true,
1919
sitemap: true,
2020
aiIndex: true,
21+
schema: true,
2122
},
2223
robots: { allow: ['/'], disallow: [], crawlDelay: 0, sitemap: '' },
2324
widget: {
@@ -29,6 +30,17 @@ describe('generateRobotsTxt', () => {
2930
showBadge: true,
3031
size: 'default' as const,
3132
},
33+
schema: {
34+
enabled: true,
35+
organization: { name: 'Test', url: 'https://example.com', logo: '', sameAs: [] },
36+
defaultType: 'WebPage',
37+
},
38+
og: {
39+
enabled: false,
40+
image: '',
41+
twitterHandle: '',
42+
type: 'website',
43+
},
3244
}
3345

3446
it('should generate robots.txt with AI crawler rules', () => {

src/core/robots.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,20 @@ export function generateRobotsTxt(config: ResolvedAeoConfig): string {
7676

7777
lines.push('# Default for all other bots');
7878
lines.push('User-agent: *');
79-
lines.push('Allow: /');
79+
for (const path of (config.robots.allow.length > 0 ? config.robots.allow : ['/'])) {
80+
lines.push(`Allow: ${path}`);
81+
}
82+
for (const path of config.robots.disallow) {
83+
lines.push(`Disallow: ${path}`);
84+
}
85+
if (config.robots.crawlDelay > 0) {
86+
lines.push(`Crawl-delay: ${config.robots.crawlDelay}`);
87+
}
8088
lines.push('');
81-
82-
if (config.url) {
83-
lines.push(`Sitemap: ${config.url}/sitemap.xml`);
89+
90+
const sitemapUrl = config.robots.sitemap || (config.url ? `${config.url}/sitemap.xml` : '');
91+
if (sitemapUrl) {
92+
lines.push(`Sitemap: ${sitemapUrl}`);
8493
}
8594

8695
lines.push('');

0 commit comments

Comments
 (0)