Add Site Builder help docs and fix FloatingHelpButton paths

- Add HelpSiteBuilder.tsx with comprehensive documentation for the
  drag-and-drop page editor (components, publishing, settings)
- Fix FloatingHelpButton to use /dashboard/help/* paths on tenant sites
- Update HelpComprehensive and HelpAutomations to rename plugins to automations
- Add site-crawler utility with cross-subdomain redirect detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-16 22:42:46 -05:00
parent 725a3c5d84
commit 94e37a2522
8 changed files with 913 additions and 189 deletions

View File

@@ -7,7 +7,7 @@ import { Page, BrowserContext } from '@playwright/test';
export interface CrawlError {
url: string;
type: 'console' | 'network' | 'broken-link' | 'page-error';
type: 'console' | 'network' | 'broken-link' | 'page-error' | 'redirect';
message: string;
details?: string;
timestamp: Date;
@@ -33,6 +33,7 @@ export interface CrawlReport {
networkErrors: number;
brokenLinks: number;
pageErrors: number;
redirects: number;
};
}
@@ -45,6 +46,8 @@ export interface CrawlerOptions {
screenshotOnError?: boolean;
screenshotDir?: string;
verbose?: boolean;
/** Detect when page redirects to a different subdomain (e.g., tenant to public) */
detectCrossSubdomainRedirects?: boolean;
}
const DEFAULT_OPTIONS: CrawlerOptions = {
@@ -63,6 +66,7 @@ const DEFAULT_OPTIONS: CrawlerOptions = {
screenshotOnError: false,
screenshotDir: 'test-results/crawler-screenshots',
verbose: false,
detectCrossSubdomainRedirects: false,
};
export class SiteCrawler {
@@ -114,6 +118,24 @@ export class SiteCrawler {
}
}
private getSubdomain(url: string): string | null {
try {
const parsed = new URL(url);
const hostname = parsed.hostname;
// Extract subdomain from *.lvh.me
if (hostname.endsWith('.lvh.me')) {
const parts = hostname.split('.');
if (parts.length >= 3) {
return parts[0];
}
}
// No subdomain (e.g., lvh.me itself)
return null;
} catch {
return null;
}
}
private shouldCrawl(url: string): boolean {
// Skip if already visited
if (this.visited.has(url)) {
@@ -280,6 +302,23 @@ export class SiteCrawler {
});
}
// Check for cross-subdomain redirects
if (this.options.detectCrossSubdomainRedirects) {
const finalUrl = this.page.url();
const requestedSubdomain = this.getSubdomain(url);
const finalSubdomain = this.getSubdomain(finalUrl);
if (requestedSubdomain && requestedSubdomain !== finalSubdomain) {
errors.push({
url,
type: 'redirect',
message: `Redirected from ${requestedSubdomain}.lvh.me to ${finalSubdomain || 'root'}.lvh.me`,
details: `Final URL: ${finalUrl}`,
timestamp: new Date(),
});
}
}
// Wait a bit for React to render and any async operations
await this.page.waitForTimeout(500);
@@ -378,6 +417,7 @@ export class SiteCrawler {
networkErrors: 0,
brokenLinks: 0,
pageErrors: 0,
redirects: 0,
};
for (const result of this.results) {
@@ -395,6 +435,9 @@ export class SiteCrawler {
case 'page-error':
summary.pageErrors++;
break;
case 'redirect':
summary.redirects++;
break;
}
}
}
@@ -427,7 +470,8 @@ export function formatReport(report: CrawlReport): string {
output += ` Console errors: ${report.summary.consoleErrors}\n`;
output += ` Network errors: ${report.summary.networkErrors}\n`;
output += ` Broken links: ${report.summary.brokenLinks}\n`;
output += ` Page errors: ${report.summary.pageErrors}\n\n`;
output += ` Page errors: ${report.summary.pageErrors}\n`;
output += ` Redirects: ${report.summary.redirects}\n\n`;
// List pages with errors
const pagesWithErrors = report.results.filter(r => r.errors.length > 0);
@@ -443,7 +487,8 @@ export function formatReport(report: CrawlReport): string {
for (const error of result.errors) {
const icon = error.type === 'console' ? '⚠️' :
error.type === 'network' ? '🌐' :
error.type === 'broken-link' ? '🔴' : '💥';
error.type === 'broken-link' ? '🔴' :
error.type === 'redirect' ? '↪️' : '💥';
output += ` ${icon} [${error.type.toUpperCase()}] ${error.message}\n`;
if (error.details) {
output += ` Details: ${error.details.substring(0, 200)}\n`;