Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions frontend/e2e/seo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,19 @@ test('seo: css display flex page keeps route identity + structured data', async
await expect(triviaMain).toContainText(/flex:\s*auto/i);
});

test('seo: login page is noindex', async ({ page }) => {
await setSeoHost(page, 'frontendatlas.com');
await page.goto('/auth/login');
await expect.poll(async () => (await getMeta(page, 'robots')) || '').toContain('noindex');
});
for (const route of ['/auth/login', '/auth/signup']) {
for (const query of ['', '?src=showcase_hero&redirectTo=%2Fdashboard']) {
test(`seo: crawlable auth page stays noindex at ${route}${query}`, async ({ page }) => {
await setSeoHost(page, 'frontendatlas.com');
await page.goto(`${route}${query}`);
const base = process.env.PLAYWRIGHT_SSR === '1'
? (process.env.PLAYWRIGHT_CANONICAL_BASE || 'https://frontendatlas.com').replace(/\/$/, '')
: new URL(page.url()).origin;
await expect.poll(() => getMeta(page, 'robots')).toBe('noindex,nofollow');
await expect.poll(() => getCanonical(page)).toBe(`${base}${route}`);
});
}
}

test('seo: preview host forces noindex', async ({ page }) => {
await setSeoHost(page, 'preview.frontendatlas.vercel.app');
Expand Down
20 changes: 19 additions & 1 deletion frontend/scripts/audit-internal-link-equity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,28 @@ test('repository robots allows clean/parameterized previews without exposing pri
assert.equal(allows(`/${family}/example/preview?entry=x&v=2`), true);
for (const suffix of ['', '/preview-extra', '/preview/child']) assert.equal(allows(`/${family}/example${suffix}`), false);
}
assert.equal(allows('/auth/login'), false);
assert.equal(allows('/dashboard'), false);
});

test('repository robots lets crawlers read login/signup noindex without opening other auth or private paths', () => {
const allows = createRobotsPolicy(fs.readFileSync(path.join(frontendRoot, 'src', 'robots.txt'), 'utf8'));
for (const route of ['/auth/login', '/auth/signup']) {
for (const suffix of ['', '?src=showcase_hero', '?redirectTo=%2Fdashboard&src=pricing']) {
assert.equal(allows(`${route}${suffix}`), true, `${route}${suffix}`);
}
for (const suffix of ['-extra', '/child', '/child?src=pricing']) {
assert.equal(allows(`${route}${suffix}`), false, `${route}${suffix}`);
}
}
for (const route of [
'/auth/callback?code=example', '/auth/forgot-password', '/auth/reset-password',
'/dashboard', '/profile?tab=activity', '/admin/users', '/tracks/foundations-30d',
'/companies/google',
]) {
assert.equal(allows(route), false, route);
}
});

test('robots combines matching groups, prefers Allow ties and respects specific agent policy', () => {
const allows = createRobotsPolicy('User-agent: *\nDisallow: /\nUser-agent: Googlebot\nDisallow: /private\nUser-agent: Googlebot\nAllow: /private\nDisallow: /secret\n');
assert.equal(allows('/'), true);
Expand Down
15 changes: 15 additions & 0 deletions frontend/scripts/sitemap.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,20 @@ function assertAngularMasteryRedirects() {
}
}

function assertLegacyVueQuestionRedirect(paths) {
const source = '/vue/trivia/vue-destructuring-breaks-reactivity-toRefs-toRef';
const destination = '/vue/trivia/vue-destructuring-breaks-reactivity-torefs-toref';
const config = JSON.parse(fs.readFileSync(VERCEL_CONFIG_PATH, 'utf8'));
const matches = config.redirects.filter((redirect) => redirect.source === source);
if (matches.length !== 1 || matches[0].destination !== destination || matches[0].permanent !== true) {
throw new Error('The legacy mixed-case Vue question URL must permanently redirect to its canonical URL.');
}
const prerender = new Set(fs.readFileSync(PRERENDER_PATH, 'utf8').split(/\r?\n/).filter(Boolean));
if (!paths.has(destination) || !prerender.has(destination) || paths.has(source) || prerender.has(source)) {
throw new Error('Only the canonical Vue question URL should be included in sitemap and prerender output.');
}
}

function assertRobotsAllowsCodingQueryNoindex() {
if (!fs.existsSync(ROBOTS_PATH)) {
throw new Error(`Missing ${ROBOTS_PATH}.`);
Expand Down Expand Up @@ -1205,6 +1219,7 @@ assertAngularHttpCancellationLabSitemapEntry(entries);
assertReactStaleClosuresSitemapEntry(entries);
assertIndexableRouteTitlesUnique();
assertAngularMasteryRedirects();
assertLegacyVueQuestionRedirect(paths);
assertRobotsAllowsCodingQueryNoindex();
assertVercelCodingQueryNoindexHeaders();
assertVercelLockedRouteNoindexHeaders();
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/robots.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
User-agent: *
Allow: /
Disallow: /auth/
Allow: /auth/login$
Allow: /auth/login?
Allow: /auth/signup$
Allow: /auth/signup?
Disallow: /dashboard
Disallow: /profile
Disallow: /admin
Expand Down
5 changes: 5 additions & 0 deletions frontend/vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@
}
],
"redirects": [
{
"source": "/vue/trivia/vue-destructuring-breaks-reactivity-toRefs-toRef",
"destination": "/vue/trivia/vue-destructuring-breaks-reactivity-torefs-toref",
"permanent": true
},
{
"source": "/guides/framework-prep/react-prep-path/mastery",
"destination": "/guides/framework-prep/react-prep-path",
Expand Down
Loading