AEO
This commit is contained in:
59
tests/e2e/canonical.spec.ts
Normal file
59
tests/e2e/canonical.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Canonical URL Validation', () => {
|
||||
test('homepage should have exactly one canonical link', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
const canonicalLinks = await page.locator('link[rel="canonical"]').all();
|
||||
expect(canonicalLinks.length).toBe(1);
|
||||
|
||||
const href = await canonicalLinks[0].getAttribute('href');
|
||||
expect(href).toBe('https://energie-profis.de/');
|
||||
});
|
||||
|
||||
test('all main pages should have proper canonical URLs', async ({ page }) => {
|
||||
const pages = [
|
||||
{ path: '/', expected: 'https://energie-profis.de/' },
|
||||
{ path: '/solar', expected: 'https://energie-profis.de/solar' },
|
||||
{ path: '/wind', expected: 'https://energie-profis.de/wind' },
|
||||
{ path: '/installateur-finden', expected: 'https://energie-profis.de/installateur-finden' },
|
||||
{ path: '/kostenlose-beratung', expected: 'https://energie-profis.de/kostenlose-beratung' },
|
||||
{ path: '/unternehmen-listen', expected: 'https://energie-profis.de/unternehmen-listen' }
|
||||
];
|
||||
|
||||
for (const { path, expected } of pages) {
|
||||
await page.goto(path);
|
||||
|
||||
// Should have exactly one canonical link
|
||||
const canonicalLinks = await page.locator('link[rel="canonical"]').all();
|
||||
expect(canonicalLinks.length).toBe(1);
|
||||
|
||||
// Should have the correct href
|
||||
const href = await canonicalLinks[0].getAttribute('href');
|
||||
expect(href).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test('canonical should not have query parameters or fragments', async ({ page }) => {
|
||||
// Test with query parameters
|
||||
await page.goto('/?utm_source=test&ref=social');
|
||||
|
||||
const canonicalLinks = await page.locator('link[rel="canonical"]').all();
|
||||
expect(canonicalLinks.length).toBe(1);
|
||||
|
||||
const href = await canonicalLinks[0].getAttribute('href');
|
||||
expect(href).toBe('https://energie-profis.de/');
|
||||
expect(href).not.toContain('utm_source');
|
||||
expect(href).not.toContain('ref=');
|
||||
});
|
||||
|
||||
test('canonical should be absolute URL with https', async ({ page }) => {
|
||||
await page.goto('/solar');
|
||||
|
||||
const canonicalLink = await page.locator('link[rel="canonical"]').first();
|
||||
const href = await canonicalLink.getAttribute('href');
|
||||
|
||||
expect(href).toMatch(/^https:\/\//);
|
||||
expect(href).toContain('energie-profis.de');
|
||||
});
|
||||
});
|
||||
37
tests/e2e/robots.spec.ts
Normal file
37
tests/e2e/robots.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Robots.txt AEO Compliance', () => {
|
||||
test('should allow PerplexityBot and GPTBot', async ({ page }) => {
|
||||
const response = await page.goto('/robots.txt');
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
const content = await page.textContent('pre') || await page.textContent('body');
|
||||
|
||||
// Check for PerplexityBot
|
||||
expect(content).toContain('User-agent: PerplexityBot');
|
||||
expect(content).toContain('Allow: /');
|
||||
|
||||
// Check for GPTBot
|
||||
expect(content).toContain('User-agent: GPTBot');
|
||||
expect(content).toContain('Allow: /');
|
||||
|
||||
// Check for sitemap reference
|
||||
expect(content).toContain('Sitemap: https://energie-profis.de/sitemap.xml');
|
||||
|
||||
// Ensure no blanket disallow that would block AI bots
|
||||
expect(content).not.toMatch(/User-agent: \*[\s\S]*?Disallow: \//);
|
||||
});
|
||||
|
||||
test('should maintain existing bot permissions', async ({ page }) => {
|
||||
const response = await page.goto('/robots.txt');
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
const content = await page.textContent('pre') || await page.textContent('body');
|
||||
|
||||
// Check that existing bots are still allowed
|
||||
expect(content).toContain('User-agent: Googlebot');
|
||||
expect(content).toContain('User-agent: Bingbot');
|
||||
expect(content).toContain('User-agent: Twitterbot');
|
||||
expect(content).toContain('User-agent: facebookexternalhit');
|
||||
});
|
||||
});
|
||||
98
tests/e2e/schema.spec.ts
Normal file
98
tests/e2e/schema.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('JSON-LD Schema Validation', () => {
|
||||
test('homepage should have valid Organization and WebSite schema', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Find JSON-LD script tags
|
||||
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all();
|
||||
expect(jsonLdScripts.length).toBeGreaterThan(0);
|
||||
|
||||
let hasWebSite = false;
|
||||
let hasOrganization = false;
|
||||
|
||||
for (const script of jsonLdScripts) {
|
||||
const content = await script.textContent();
|
||||
if (content) {
|
||||
const data = JSON.parse(content);
|
||||
|
||||
if (data['@type'] === 'WebSite') {
|
||||
hasWebSite = true;
|
||||
expect(data.name).toBe('EnergieProfis');
|
||||
expect(data.url).toMatch(/energie-profis\.de/);
|
||||
expect(data.potentialAction).toBeDefined();
|
||||
expect(data.potentialAction['@type']).toBe('SearchAction');
|
||||
}
|
||||
|
||||
if (data['@type'] === 'Organization') {
|
||||
hasOrganization = true;
|
||||
expect(data.name).toBe('EnergieProfis');
|
||||
expect(data.logo).toBeDefined();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(hasWebSite).toBe(true);
|
||||
expect(hasOrganization).toBe(true);
|
||||
});
|
||||
|
||||
test('FAQ sections should have valid FAQPage schema', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Look for FAQ content on homepage
|
||||
const faqSection = await page.locator('text=Häufige Fragen').first();
|
||||
if (await faqSection.isVisible()) {
|
||||
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all();
|
||||
|
||||
let hasFAQPage = false;
|
||||
for (const script of jsonLdScripts) {
|
||||
const content = await script.textContent();
|
||||
if (content) {
|
||||
const data = JSON.parse(content);
|
||||
|
||||
if (data['@type'] === 'FAQPage') {
|
||||
hasFAQPage = true;
|
||||
expect(data.mainEntity).toBeInstanceOf(Array);
|
||||
expect(data.mainEntity.length).toBeGreaterThan(0);
|
||||
|
||||
// Validate each FAQ item
|
||||
for (const item of data.mainEntity) {
|
||||
expect(item['@type']).toBe('Question');
|
||||
expect(item.name).toBeDefined();
|
||||
expect(item.acceptedAnswer).toBeDefined();
|
||||
expect(item.acceptedAnswer['@type']).toBe('Answer');
|
||||
expect(item.acceptedAnswer.text).toBeDefined();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FAQ schema should exist if FAQ content is present
|
||||
if (await faqSection.isVisible()) {
|
||||
expect(hasFAQPage).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('all JSON-LD should be valid JSON', async ({ page }) => {
|
||||
const pages = ['/', '/solar', '/wind', '/installateur-finden'];
|
||||
|
||||
for (const pagePath of pages) {
|
||||
await page.goto(pagePath);
|
||||
|
||||
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all();
|
||||
|
||||
for (const script of jsonLdScripts) {
|
||||
const content = await script.textContent();
|
||||
if (content) {
|
||||
// This should not throw if JSON is valid
|
||||
expect(() => JSON.parse(content)).not.toThrow();
|
||||
|
||||
const data = JSON.parse(content);
|
||||
expect(data['@context']).toBe('https://schema.org');
|
||||
expect(data['@type']).toBeDefined();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
77
tests/e2e/sitemap.spec.ts
Normal file
77
tests/e2e/sitemap.spec.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Sitemap XML Validation', () => {
|
||||
test('should serve valid sitemap.xml', async ({ page }) => {
|
||||
const response = await page.goto('/sitemap.xml');
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(response?.headers()['content-type']).toMatch(/xml/);
|
||||
|
||||
const content = await page.textContent('urlset') || await page.textContent('body');
|
||||
|
||||
// Should be valid XML
|
||||
expect(content).toContain('<?xml version="1.0" encoding="UTF-8"?>');
|
||||
expect(content).toContain('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
|
||||
expect(content).toContain('</urlset>');
|
||||
});
|
||||
|
||||
test('should include all main pages with lastmod', async ({ page }) => {
|
||||
await page.goto('/sitemap.xml');
|
||||
|
||||
const content = await page.textContent('urlset') || await page.textContent('body');
|
||||
|
||||
const expectedUrls = [
|
||||
'https://energie-profis.de/',
|
||||
'https://energie-profis.de/solar',
|
||||
'https://energie-profis.de/wind',
|
||||
'https://energie-profis.de/installateur-finden',
|
||||
'https://energie-profis.de/kostenlose-beratung',
|
||||
'https://energie-profis.de/unternehmen-listen'
|
||||
];
|
||||
|
||||
for (const url of expectedUrls) {
|
||||
expect(content).toContain(`<loc>${url}</loc>`);
|
||||
}
|
||||
|
||||
// Check for lastmod tags
|
||||
expect(content).toMatch(/<lastmod>\d{4}-\d{2}-\d{2}<\/lastmod>/);
|
||||
});
|
||||
|
||||
test('should have proper priority and changefreq values', async ({ page }) => {
|
||||
await page.goto('/sitemap.xml');
|
||||
|
||||
const content = await page.textContent('urlset') || await page.textContent('body');
|
||||
|
||||
// Homepage should have highest priority
|
||||
expect(content).toMatch(/https:\/\/energie-profis\.de\/<\/loc>[\s\S]*?<priority>1\.0<\/priority>/);
|
||||
|
||||
// Should have valid changefreq values
|
||||
const validChangeFreqs = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'];
|
||||
const changefreqMatches = content.match(/<changefreq>([^<]+)<\/changefreq>/g) || [];
|
||||
|
||||
for (const match of changefreqMatches) {
|
||||
const value = match.replace(/<\/?changefreq>/g, '');
|
||||
expect(validChangeFreqs).toContain(value);
|
||||
}
|
||||
});
|
||||
|
||||
test('should have valid date format for lastmod', async ({ page }) => {
|
||||
await page.goto('/sitemap.xml');
|
||||
|
||||
const content = await page.textContent('urlset') || await page.textContent('body');
|
||||
|
||||
// Extract all lastmod dates
|
||||
const lastmodMatches = content.match(/<lastmod>([^<]+)<\/lastmod>/g) || [];
|
||||
expect(lastmodMatches.length).toBeGreaterThan(0);
|
||||
|
||||
for (const match of lastmodMatches) {
|
||||
const dateString = match.replace(/<\/?lastmod>/g, '');
|
||||
|
||||
// Should be in YYYY-MM-DD format (ISO date)
|
||||
expect(dateString).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
|
||||
// Should be a valid date
|
||||
const date = new Date(dateString);
|
||||
expect(date.toString()).not.toBe('Invalid Date');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user