From bbda4d699cd834c02db73d06e9f8c3cae1265828 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 14 Apr 2026 19:22:33 +0000 Subject: [PATCH] Autosave: 20260414-192235 --- frontend/src/components/Logo/index.tsx | 11 +- frontend/src/pages/profile.tsx | 9 +- frontend/src/pages/sites/analyzer.tsx | 1445 +++++++++++++++++++----- 3 files changed, 1201 insertions(+), 264 deletions(-) diff --git a/frontend/src/components/Logo/index.tsx b/frontend/src/components/Logo/index.tsx index a582e29..30747d0 100644 --- a/frontend/src/components/Logo/index.tsx +++ b/frontend/src/components/Logo/index.tsx @@ -1,3 +1,4 @@ +import Image from 'next/image' import React from 'react' type Props = { @@ -6,10 +7,12 @@ type Props = { export default function Logo({ className = '' }: Props) { return ( - {'Flatlogic - + alt="Flatlogic logo" + width={160} + height={32} + /> ) } diff --git a/frontend/src/pages/profile.tsx b/frontend/src/pages/profile.tsx index f5eb7cf..000ed48 100644 --- a/frontend/src/pages/profile.tsx +++ b/frontend/src/pages/profile.tsx @@ -3,6 +3,7 @@ import { mdiUpload, } from '@mdi/js'; import Head from 'next/head'; +import Image from 'next/image'; import React, { ReactElement, useEffect, useState } from 'react'; import { ToastContainer, toast } from 'react-toastify'; import DatePicker from 'react-datepicker'; @@ -84,7 +85,13 @@ const EditUsers = () => { {currentUser?.avatar[0]?.publicUrl &&
- Avatar + Avatar
} void; @@ -147,11 +150,36 @@ type SetupAccordionSectionProps = { type ResultsTabButtonProps = { label: string; + iconPath: string; count?: number | string; isActive: boolean; onClick: () => void; }; +type PageFilterChipProps = { + label: string; + count: number; + iconPath: string; + isActive: boolean; + onClick: () => void; +}; + +type DeliverySummaryCardProps = { + label: string; + value: string | number; + helper: string; + iconPath: string; + toneClassName?: string; +}; + +type DeliveryActionCardProps = { + title: string; + description: string; + iconPath: string; + badge?: React.ReactNode; + children: React.ReactNode; +}; + const PLATFORM_OPTIONS = [ { value: 'wordpress', label: 'WordPress' }, { value: 'shopify', label: 'Shopify' }, @@ -166,31 +194,163 @@ const parseTargetLines = (value: string) => value .map((entry) => entry.trim()) .filter(Boolean); +const recommendationPriorityOrder = ['critical', 'high', 'medium', 'low', 'other'] as const; +type RecommendationPriorityId = (typeof recommendationPriorityOrder)[number]; + +type RecommendationPriorityMeta = { + id: RecommendationPriorityId; + label: string; + sortOrder: number; + iconPath: string; + badgeClassName: string; + sectionTitle: string; + sectionDescription: string; + accentClassName: string; +}; + +const recommendationPriorityMetaMap: Record = { + critical: { + id: 'critical', + label: 'Critical', + sortOrder: 0, + iconPath: icon.mdiAlertCircleOutline, + badgeClassName: 'bg-rose-600 text-white dark:bg-rose-500 dark:text-white', + sectionTitle: 'Critical fixes first', + sectionDescription: 'Resolve these before anything else because they are the most urgent structured data gaps.', + accentClassName: 'border-rose-200 bg-rose-50/80 dark:border-rose-500/30 dark:bg-rose-500/10', + }, + high: { + id: 'high', + label: 'High', + sortOrder: 1, + iconPath: icon.mdiAlertOutline, + badgeClassName: 'bg-amber-500 text-white dark:bg-amber-400 dark:text-slate-950', + sectionTitle: 'High priority', + sectionDescription: 'These recommendations should be tackled early because they likely affect key pages or important schema coverage.', + accentClassName: 'border-amber-200 bg-amber-50/80 dark:border-amber-500/30 dark:bg-amber-500/10', + }, + medium: { + id: 'medium', + label: 'Medium', + sortOrder: 2, + iconPath: icon.mdiArrowDownCircleOutline, + badgeClassName: 'bg-sky-600 text-white dark:bg-sky-500 dark:text-white', + sectionTitle: 'Next up', + sectionDescription: 'Address these after the urgent items to improve broader coverage and quality.', + accentClassName: 'border-sky-200 bg-sky-50/80 dark:border-sky-500/30 dark:bg-sky-500/10', + }, + low: { + id: 'low', + label: 'Low', + sortOrder: 3, + iconPath: icon.mdiCheckCircleOutline, + badgeClassName: 'bg-emerald-600 text-white dark:bg-emerald-500 dark:text-white', + sectionTitle: 'Quick wins', + sectionDescription: 'Useful polish items that can be handled once higher-impact fixes are moving.', + accentClassName: 'border-emerald-200 bg-emerald-50/80 dark:border-emerald-500/30 dark:bg-emerald-500/10', + }, + other: { + id: 'other', + label: 'Unprioritized', + sortOrder: 4, + iconPath: icon.mdiLightbulbOutline, + badgeClassName: 'bg-slate-800 text-white dark:bg-slate-200 dark:text-slate-950', + sectionTitle: 'More opportunities', + sectionDescription: 'These are useful follow-up ideas that were not assigned a stronger priority label.', + accentClassName: 'border-slate-200 bg-slate-50/80 dark:border-slate-700 dark:bg-slate-900/40', + }, +}; + +const normalizeRecommendationPriority = (priority?: string): RecommendationPriorityId => { + const normalizedPriority = priority?.trim().toLowerCase(); + + if (!normalizedPriority) { + return 'other'; + } + + if (normalizedPriority.includes('critical') || normalizedPriority === 'p0') { + return 'critical'; + } + + if (normalizedPriority.includes('high') || normalizedPriority === 'p1') { + return 'high'; + } + + if (normalizedPriority.includes('medium') || normalizedPriority.includes('med') || normalizedPriority === 'p2') { + return 'medium'; + } + + if (normalizedPriority.includes('low') || normalizedPriority === 'p3') { + return 'low'; + } + + return 'other'; +}; + +const getRecommendationPriorityMeta = (priority?: string) => recommendationPriorityMetaMap[normalizeRecommendationPriority(priority)]; + +const isFixFirstRecommendation = (recommendation: Recommendation) => { + const priorityId = normalizeRecommendationPriority(recommendation.priority); + return priorityId === 'critical' || priorityId === 'high'; +}; + +const getRecommendationScopeSortOrder = (pageScope?: string) => { + const normalizedScope = pageScope?.trim().toLowerCase() || ''; + + if (!normalizedScope) { + return 4; + } + + if (normalizedScope.includes('site') || normalizedScope.includes('global') || normalizedScope.includes('all')) { + return 0; + } + + if (normalizedScope.includes('home')) { + return 1; + } + + if (normalizedScope.includes('template') || normalizedScope.includes('category') || normalizedScope.includes('collection') || normalizedScope.includes('product')) { + return 2; + } + + if (normalizedScope.includes('page')) { + return 3; + } + + return 4; +}; + const SetupAccordionSection = ({ title, description, + iconPath, badge, isOpen, onToggle, children, }: SetupAccordionSectionProps) => ( -
+
+); + +const DeliverySummaryCard = ({ + label, + value, + helper, + iconPath, + toneClassName = 'border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-950/40', +}: DeliverySummaryCardProps) => ( +
+
+
+
{label}
+
{value}
+
+ + + +
+

{helper}

+
+); + +const DeliveryActionCard = ({ title, description, iconPath, badge, children }: DeliveryActionCardProps) => ( +
+
+
+ + + +
+
{title}
+

{description}

+
+
+ {badge} +
+
{children}
+
+); + const SchemaAnalyzerPage = () => { const { currentUser } = useAppSelector((state) => state.auth); const [url, setUrl] = React.useState(''); @@ -241,11 +459,20 @@ const SchemaAnalyzerPage = () => { const [exportingId, setExportingId] = React.useState(null); const [isCheckingPlatformOutput, setIsCheckingPlatformOutput] = React.useState(false); const [openSections, setOpenSections] = React.useState>({ - targeting: true, + targeting: false, options: false, limits: false, }); const [activeResultsTab, setActiveResultsTab] = React.useState('overview'); + const [activePageFilter, setActivePageFilter] = React.useState('all'); + const [activeRecommendationFilter, setActiveRecommendationFilter] = React.useState('all'); + const [isFailedPagesExpanded, setIsFailedPagesExpanded] = React.useState(false); + const [expandedRecommendationIds, setExpandedRecommendationIds] = React.useState>({}); + const resultsRef = React.useRef(null); + + const scrollToResults = React.useCallback(() => { + resultsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, []); React.useEffect(() => { if (currentUser?.email) { @@ -254,10 +481,22 @@ const SchemaAnalyzerPage = () => { }, [currentUser?.email]); React.useEffect(() => { - if (report?.analysis) { - setActiveResultsTab('overview'); + if (!report?.analysis) { + return undefined; } - }, [report?.analysis?.analyzedUrl, report?.analysis?.fetchedAt]); + + setActiveResultsTab('overview'); + setActivePageFilter('all'); + setActiveRecommendationFilter('all'); + setIsFailedPagesExpanded(false); + setExpandedRecommendationIds({}); + + const timeoutId = window.setTimeout(() => { + scrollToResults(); + }, 150); + + return () => window.clearTimeout(timeoutId); + }, [report?.analysis?.analyzedUrl, report?.analysis?.fetchedAt, scrollToResults]); const notify = React.useCallback((type: 'success' | 'error' | 'info', message: string) => { toast(message, { type, position: 'bottom-center' }); @@ -273,6 +512,30 @@ const SchemaAnalyzerPage = () => { const exportableRecommendations = recommendations.filter( (recommendation) => recommendation.suggested_schema, ); + const sortedRecommendations = React.useMemo(() => ( + [...recommendations].sort((leftRecommendation, rightRecommendation) => { + const leftPriority = getRecommendationPriorityMeta(leftRecommendation.priority); + const rightPriority = getRecommendationPriorityMeta(rightRecommendation.priority); + + if (leftPriority.sortOrder !== rightPriority.sortOrder) { + return leftPriority.sortOrder - rightPriority.sortOrder; + } + + const leftScopeOrder = getRecommendationScopeSortOrder(leftRecommendation.page_scope); + const rightScopeOrder = getRecommendationScopeSortOrder(rightRecommendation.page_scope); + if (leftScopeOrder !== rightScopeOrder) { + return leftScopeOrder - rightScopeOrder; + } + + const leftHasCode = Number(Boolean(leftRecommendation.suggested_schema)); + const rightHasCode = Number(Boolean(rightRecommendation.suggested_schema)); + if (leftHasCode !== rightHasCode) { + return rightHasCode - leftHasCode; + } + + return leftRecommendation.title.localeCompare(rightRecommendation.title); + }) + ), [recommendations]); const crawlPlan = report?.analysis?.crawlPlan; const isRequestedPagesOverLimit = requestedPages > maxPagesPerCrawl; const draftIncludeTargets = React.useMemo(() => parseTargetLines(includeTargets), [includeTargets]); @@ -290,6 +553,230 @@ const SchemaAnalyzerPage = () => { const analyzedTimestamp = report?.analysis?.fetchedAt ? new Date(report.analysis.fetchedAt).toLocaleString() : null; + const hasEmailRecipient = emailTo.trim().length > 0; + const hasUrl = url.trim().length > 0; + const targetingSummary = hasTargetingRules + ? `${appliedIncludeTargets.length} include · ${appliedExcludeTargets.length} exclude` + : 'No targeting rules'; + const recommendationQuickFilters = [ + { + id: 'all' as const, + label: 'All', + count: recommendations.length, + iconPath: icon.mdiViewListOutline, + }, + { + id: 'fixFirst' as const, + label: 'Fix first', + count: sortedRecommendations.filter((recommendation) => isFixFirstRecommendation(recommendation)).length, + iconPath: icon.mdiAlertCircleOutline, + }, + { + id: 'codeReady' as const, + label: 'Code ready', + count: sortedRecommendations.filter((recommendation) => recommendation.suggested_schema).length, + iconPath: icon.mdiCodeBraces, + }, + { + id: 'needsCode' as const, + label: 'Needs code', + count: sortedRecommendations.filter((recommendation) => !recommendation.suggested_schema).length, + iconPath: icon.mdiLightbulbOutline, + }, + ]; + const pageFilterOptions = [ + { + id: 'all' as const, + label: 'All', + count: analyzedPages.length + failedPages.length, + iconPath: icon.mdiViewListOutline, + }, + { + id: 'withSchema' as const, + label: 'With schema', + count: analyzedPages.filter((page) => page.hasStructuredData).length, + iconPath: icon.mdiCheckCircleOutline, + }, + { + id: 'missingSchema' as const, + label: 'Missing schema', + count: analyzedPages.filter((page) => !page.hasStructuredData).length, + iconPath: icon.mdiAlertCircleOutline, + }, + { + id: 'failed' as const, + label: 'Failed', + count: failedPages.length, + iconPath: icon.mdiCloseCircleOutline, + }, + ]; + const filteredRecommendations = React.useMemo(() => { + if (activeRecommendationFilter === 'fixFirst') { + return sortedRecommendations.filter((recommendation) => isFixFirstRecommendation(recommendation)); + } + + if (activeRecommendationFilter === 'codeReady') { + return sortedRecommendations.filter((recommendation) => recommendation.suggested_schema); + } + + if (activeRecommendationFilter === 'needsCode') { + return sortedRecommendations.filter((recommendation) => !recommendation.suggested_schema); + } + + return sortedRecommendations; + }, [activeRecommendationFilter, sortedRecommendations]); + const recommendationGroups = React.useMemo(() => recommendationPriorityOrder + .map((priorityId) => ({ + meta: recommendationPriorityMetaMap[priorityId], + recommendations: filteredRecommendations.filter( + (recommendation) => normalizeRecommendationPriority(recommendation.priority) === priorityId, + ), + })) + .filter((group) => group.recommendations.length > 0), [filteredRecommendations]); + const activeRecommendationFilterLabel = recommendationQuickFilters.find( + (filterOption) => filterOption.id === activeRecommendationFilter, + )?.label || 'All'; + const recommendationEmptyStateMessage = activeRecommendationFilter === 'fixFirst' + ? 'No high-priority recommendations are waiting in this report.' + : activeRecommendationFilter === 'codeReady' + ? 'No recommendations with generated code are available yet.' + : activeRecommendationFilter === 'needsCode' + ? 'Every visible recommendation already has a code snippet attached.' + : 'No recommendations were generated for this page yet.'; + + const filteredAnalyzedPages = React.useMemo(() => { + if (activePageFilter === 'withSchema') { + return analyzedPages.filter((page) => page.hasStructuredData); + } + + if (activePageFilter === 'missingSchema') { + return analyzedPages.filter((page) => !page.hasStructuredData); + } + + if (activePageFilter === 'failed') { + return []; + } + + return analyzedPages; + }, [activePageFilter, analyzedPages]); + const shouldShowFailedSection = failedPages.length > 0 && (activePageFilter === 'all' || activePageFilter === 'failed'); + const emptyPagesStateMessage = activePageFilter === 'failed' + ? 'No failed internal pages were recorded for this analysis run.' + : activePageFilter === 'withSchema' + ? 'No analyzed pages with structured data match this filter yet.' + : activePageFilter === 'missingSchema' + ? 'No analyzed pages are missing structured data for this run.' + : 'No page-level results are available yet for this analysis run.'; + const deliverySummaryCards = [ + { + label: 'Code-ready fixes', + value: exportableRecommendations.length, + helper: exportableRecommendations.length > 0 + ? `${exportableRecommendations.length} recommendation${exportableRecommendations.length === 1 ? '' : 's'} can be exported right now.` + : 'No code-ready recommendations yet. Use the Recommendations tab to refine the handoff.', + iconPath: icon.mdiCodeBraces, + toneClassName: exportableRecommendations.length > 0 + ? 'border-emerald-200 bg-emerald-50/80 dark:border-emerald-500/30 dark:bg-emerald-500/10' + : 'border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-950/40', + }, + { + label: 'Email recipient', + value: hasEmailRecipient ? 'Ready' : 'Missing', + helper: hasEmailRecipient ? emailTo.trim() : 'Add a developer email before sending the handoff.', + iconPath: hasEmailRecipient ? icon.mdiEmailOutline : icon.mdiAlertCircleOutline, + toneClassName: hasEmailRecipient + ? 'border-sky-200 bg-sky-50/80 dark:border-sky-500/30 dark:bg-sky-500/10' + : 'border-amber-200 bg-amber-50/80 dark:border-amber-500/30 dark:bg-amber-500/10', + }, + { + label: 'Platform output', + value: entitlements?.canPlatformOutput ? 'Unlocked' : 'Premium', + helper: entitlements?.canPlatformOutput + ? `${selectedPlatformLabel} output can be checked in Step 4.` + : 'Premium is required for Step 4 platform-specific output.', + iconPath: entitlements?.canPlatformOutput ? icon.mdiCheckCircleOutline : icon.mdiLockOutline, + toneClassName: entitlements?.canPlatformOutput + ? 'border-violet-200 bg-violet-50/80 dark:border-violet-500/30 dark:bg-violet-500/10' + : 'border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-950/40', + }, + ]; + const deliveryChecklist = [ + { + id: 'recipient', + label: 'Recipient email', + value: hasEmailRecipient ? emailTo.trim() : 'Add an email to send the handoff.', + isReady: hasEmailRecipient, + }, + { + id: 'export', + label: 'Export package', + value: exportableRecommendations.length > 0 + ? `${exportableRecommendations.length} code-ready recommendation${exportableRecommendations.length === 1 ? '' : 's'} available.` + : 'Export all still works, but no generated code is attached yet.', + isReady: Boolean(report?.site?.id), + }, + { + id: 'platform', + label: 'Step 4 output', + value: entitlements?.canPlatformOutput + ? `${selectedPlatformLabel} output is available for this workspace.` + : `${selectedPlatformLabel} output requires Premium access.`, + isReady: Boolean(entitlements?.canPlatformOutput), + }, + ]; + const overviewStats = [ + { + label: 'Pages analyzed', + value: crawlPlan?.actualPagesAnalyzed || analyzedPages.length || 0, + helper: 'Crawl total', + iconPath: icon.mdiFileDocumentOutline, + }, + { + label: 'Recommendations', + value: recommendations.length, + helper: 'Next actions', + iconPath: icon.mdiLightbulbOutline, + }, + { + label: 'Structured data', + value: report?.analysis?.crawlSummary?.pagesWithStructuredData ?? (report?.analysis?.schema?.hasStructuredData ? 1 : 0), + helper: 'Pages with schema', + iconPath: icon.mdiCheckCircleOutline, + }, + { + label: 'JSON-LD blocks', + value: report?.analysis?.schema?.jsonLd?.count || 0, + helper: 'Detected snippets', + iconPath: icon.mdiCodeJson, + }, + { + label: 'Failed fetches', + value: report?.analysis?.crawlSummary?.failedPages ?? failedPages.length, + helper: 'Needs follow-up', + iconPath: icon.mdiAlertCircleOutline, + }, + { + label: 'Invalid blocks', + value: invalidJsonLdBlocks.length, + helper: 'Needs cleanup', + iconPath: icon.mdiAlertOutline, + }, + ]; + + React.useEffect(() => { + if ((draftIncludeTargets.length > 0 || draftExcludeTargets.length > 0) && !openSections.targeting) { + setOpenSections((currentSections) => ({ + ...currentSections, + targeting: true, + })); + } + }, [draftExcludeTargets.length, draftIncludeTargets.length, openSections.targeting]); + + React.useEffect(() => { + if (activePageFilter === 'failed' && failedPages.length > 0) { + setIsFailedPagesExpanded(true); + } + }, [activePageFilter, failedPages.length]); const toggleSection = (section: SetupSectionId) => { setOpenSections((currentSections) => ({ @@ -298,6 +785,13 @@ const SchemaAnalyzerPage = () => { })); }; + const toggleRecommendationCode = (recommendationId: string) => { + setExpandedRecommendationIds((currentIds) => ({ + ...currentIds, + [recommendationId]: !currentIds[recommendationId], + })); + }; + const handleAnalyze = async () => { if (!url.trim()) { notify('error', 'Enter a website URL first.'); @@ -492,18 +986,18 @@ const SchemaAnalyzerPage = () => { {''} - -
+ +

Analyze a customer site

-

+

Enter a domain or full URL, choose how many pages to review, and optionally focus the report on the folders, categories, or pages that matter most. This setup keeps the page cleaner on mobile while still supporting up to {maxPagesPerCrawl} pages per crawl.

-
+
{ /> -
+
{ /> - - { - handleAnalyze().catch(() => null); - }} - /> - +
+
Quick setup
+
Pick a page count here, then use Target pages below if you want a more focused report.
+
@@ -567,11 +1054,12 @@ const SchemaAnalyzerPage = () => { toggleSection('targeting')} > -
+
-
+
Accepted formats
Full URLs or path rules like /blog, /pricing, or /category/shoes.
@@ -629,7 +1117,7 @@ https://example.com/pricing`} {appliedIncludeTargets.map((target) => ( {target} @@ -645,7 +1133,7 @@ https://example.com/pricing`} {appliedExcludeTargets.map((target) => ( {target} @@ -657,9 +1145,70 @@ https://example.com/pricing`} )} +
+
+
+
+
Ready to analyze
+
Run a focused crawl when you are ready.
+

+ Review the target pages above, then launch the next crawl. You can analyze up to {maxPagesPerCrawl} pages per run. +

+
+
+ + {requestedPages} requested + + + {targetingSummary} + +
+
+
+ + {report?.analysis && ( + + )} + { + handleAnalyze().catch(() => null); + }} + /> + +

+ {isRequestedPagesOverLimit + ? `Reduce to ${maxPagesPerCrawl} pages to continue.` + : report?.analysis + ? 'Runs the latest crawl, then jumps you straight to the refreshed results.' + : hasUrl + ? 'Runs the latest crawl and opens the refreshed results below.' + : 'Add a website URL above to enable the analyzer.'} +

+
+
+
+ toggleSection('options')} @@ -699,6 +1248,7 @@ https://example.com/pricing`} toggleSection('limits')} @@ -728,8 +1278,32 @@ https://example.com/pricing`}
+
+
+
+
+ {hasUrl ? url.trim() : 'Enter a website URL'} +
+
+ {requestedPages} page{requestedPages === 1 ? '' : 's'} requested · {targetingSummary} +
+
+ { + handleAnalyze().catch(() => null); + }} + /> +
+
+ {report?.analysis && ( - +
+

Analysis results

@@ -749,27 +1323,31 @@ https://example.com/pricing`}
-
-
+
+
setActiveResultsTab('overview')} /> setActiveResultsTab('pages')} /> setActiveResultsTab('recommendations')} /> setActiveResultsTab('delivery')} /> @@ -779,70 +1357,104 @@ https://example.com/pricing`}
{activeResultsTab === 'overview' && (
-
-
-
Analyzed URL
-
- {report.analysis.analyzedUrl || report.site?.base_url || '—'} +
+
+
+ + + +
+
Site snapshot
+
+ {report.analysis.analyzedUrl || report.site?.base_url || '—'} +
+
+ {report.analysis.pageTitle || 'No page title found for the analyzed page yet.'} +
+
+
+ +
+
+
Platform
+
+ {report.analysis.platform?.label || 'Unknown platform'} +
+
+
+
Last updated
+
+ {analyzedTimestamp || 'Just now'} +
+
-
-
Page title
-
- {report.analysis.pageTitle || 'No title found'} -
-
-
-
Recommendations
-
- {recommendations.length} total -
-
-
-
Pages analyzed
-
- {crawlPlan?.actualPagesAnalyzed || analyzedPages.length || 0} -
-
-
-
Pages with structured data
-
- {report.analysis.crawlSummary?.pagesWithStructuredData ?? (report.analysis.schema?.hasStructuredData ? 1 : 0)} -
-
-
-
JSON-LD blocks found
-
- {report.analysis.schema?.jsonLd?.count || 0} -
+ +
+ {overviewStats.map((stat) => ( +
+
+
+
{stat.label}
+
+ {stat.value} +
+
{stat.helper}
+
+ + + +
+
+ ))}
{crawlPlan && (
-
Crawl summary
-
- Requested pages: {crawlPlan.requestedPages || 1} - Plan limit: {crawlPlan.allowedPages || maxPagesPerCrawl} - Pages analyzed: {crawlPlan.actualPagesAnalyzed || 0} - Failed page fetches: {report.analysis.crawlSummary?.failedPages ?? failedPages.length} +
+
Crawl summary
+
+ Focused for mobile review +
- {report.analysis.notice &&
{report.analysis.notice}
} +
+
+
Requested
+
{crawlPlan.requestedPages || 1}
+
+
+
Plan limit
+
{crawlPlan.allowedPages || maxPagesPerCrawl}
+
+
+
Analyzed
+
{crawlPlan.actualPagesAnalyzed || 0}
+
+
+
Failed
+
{report.analysis.crawlSummary?.failedPages ?? failedPages.length}
+
+
+ {report.analysis.notice &&
{report.analysis.notice}
}
)} {report.analysis.crawlSummary && ( -
+
Pages without structured data
-
+
{report.analysis.crawlSummary.pagesWithoutStructuredData ?? 0}
-
Invalid JSON-LD blocks
-
- {invalidJsonLdBlocks.length} +
Discovered internal pages
+
+ {report.analysis.crawlSummary.discoveredInternalPages ?? analyzedPages.length}
@@ -925,42 +1537,114 @@ https://example.com/pricing`} {activeResultsTab === 'pages' && (
- {analyzedPages.length === 0 && failedPages.length === 0 && ( +
+
+
+
Quick filters
+

+ Narrow the page list to the pages that need attention most on mobile. +

+
+
+ {pageFilterOptions.map((filterOption) => ( + setActivePageFilter(filterOption.id)} + /> + ))} +
+
+
+ {pageFilterOptions.map((filterOption) => ( + setActivePageFilter(filterOption.id)} + /> + ))} +
+
+ + {filteredAnalyzedPages.length === 0 && !shouldShowFailedSection && (
- No page-level results are available yet for this analysis run. + {emptyPagesStateMessage}
)} - {analyzedPages.length > 0 && ( + {filteredAnalyzedPages.length > 0 && (
-
Analyzed pages
+
+
+ {activePageFilter === 'withSchema' + ? 'Pages with structured data' + : activePageFilter === 'missingSchema' + ? 'Pages missing structured data' + : 'Analyzed pages'} +
+
+ {filteredAnalyzedPages.length} result{filteredAnalyzedPages.length === 1 ? '' : 's'} +
+
- {analyzedPages.map((page) => ( + {filteredAnalyzedPages.map((page) => (
-
{page.url}
-
- {page.title || 'Untitled page'} -
-
- - Status {page.statusCode || '—'} - - - {page.hasStructuredData ? 'Structured data found' : 'No structured data'} - - {(page.jsonLdTypes || []).slice(0, 3).map((typeName) => ( - - {typeName} +
+
+
+ {page.title || 'Untitled page'} +
+
+ {page.url} +
+
+
+ + Status {page.statusCode || '—'} - ))} + + {page.hasStructuredData ? 'Structured data found' : 'Needs schema'} + +
+
+ +
+
+
Page status
+
+ {page.hasStructuredData + ? 'Structured data was detected on this page.' + : 'No structured data was detected yet for this page.'} +
+
+
+
Schema types
+ {(page.jsonLdTypes || []).length > 0 ? ( +
+ {(page.jsonLdTypes || []).slice(0, 4).map((typeName) => ( + + {typeName} + + ))} +
+ ) : ( +
No JSON-LD types were detected on this page.
+ )} +
))} @@ -968,16 +1652,42 @@ https://example.com/pricing`}
)} - {failedPages.length > 0 && ( -
-
Some internal pages could not be fetched
-
    - {failedPages.map((page) => ( -
  • - {page.url}: {page.error} -
  • - ))} -
+ {shouldShowFailedSection && ( +
+ + + {isFailedPagesExpanded && ( +
+ {failedPages.map((page) => ( +
+
{page.url}
+
{page.error}
+
+ ))} +
+ )}
)}
@@ -986,15 +1696,21 @@ https://example.com/pricing`} {activeResultsTab === 'recommendations' && (
-
- {recommendations.length} recommendation{recommendations.length === 1 ? '' : 's'} generated from the latest analysis. +
+
+ {recommendations.length} recommendation{recommendations.length === 1 ? '' : 's'} generated from the latest analysis. +
+
+ Fix-first order puts higher priority and broader-scope recommendations at the top. +
- + { const combined = exportableRecommendations @@ -1013,87 +1729,186 @@ https://example.com/pricing`}
- {recommendations.length === 0 && ( -
- No recommendations were generated for this page yet. + {recommendations.length > 0 && ( +
+
+ {recommendationQuickFilters.map((filterOption) => ( + setActiveRecommendationFilter(filterOption.id)} + /> + ))} +
+
+ Showing {filteredRecommendations.length} item{filteredRecommendations.length === 1 ? '' : 's'} in the {activeRecommendationFilterLabel} view. +
)} - {recommendations.map((recommendation) => ( -
-
-
-
- - {recommendation.priority || 'priority'} - - {recommendation.schema_type && ( - - {recommendation.schema_type} - - )} - {recommendation.page_scope && ( - - {recommendation.page_scope} - - )} -
-

- {recommendation.title} -

-

- {recommendation.reason} -

- {recommendation.expected_impact && ( -

- Expected impact:{' '} - {recommendation.expected_impact} -

- )} -
+ {filteredRecommendations.length === 0 && ( +
+ {recommendationEmptyStateMessage} +
+ )} - - { - handleCopyCode(recommendation).catch(() => null); - }} - /> - { - handleExportRecommendation(recommendation).catch(() => null); - }} - /> - { - handleEmailCode(recommendation.id).catch(() => null); - }} - /> - + {recommendationGroups.map((recommendationGroup) => ( +
+
+
+
+
+ + + +
+

{recommendationGroup.meta.sectionTitle}

+

{recommendationGroup.meta.sectionDescription}

+
+
+
+ + {recommendationGroup.recommendations.length} item{recommendationGroup.recommendations.length === 1 ? '' : 's'} + +
-
-
Suggested code
-
-                          {recommendation.suggested_schema || 'No code snippet generated for this recommendation.'}
-                        
+
+ {recommendationGroup.recommendations.map((recommendation) => { + const isCodeExpanded = !!expandedRecommendationIds[recommendation.id]; + const suggestedSchema = recommendation.suggested_schema || ''; + const codePreview = suggestedSchema.split('\n').slice(0, 4).join('\n'); + const codeLineCount = suggestedSchema ? suggestedSchema.split('\n').length : 0; + const priorityMeta = getRecommendationPriorityMeta(recommendation.priority); + + return ( +
+
+
+
+ + {priorityMeta.label} + + {recommendation.schema_type && ( + + {recommendation.schema_type} + + )} + {recommendation.page_scope && ( + + {recommendation.page_scope} + + )} + + {recommendation.suggested_schema ? 'Code ready' : 'Needs code'} + +
+

+ {recommendation.title} +

+

+ {recommendation.reason} +

+ {recommendation.expected_impact && ( +

+ Expected impact:{' '} + {recommendation.expected_impact} +

+ )} +
+ + + { + handleCopyCode(recommendation).catch(() => null); + }} + /> + { + handleExportRecommendation(recommendation).catch(() => null); + }} + /> + { + handleEmailCode(recommendation.id).catch(() => null); + }} + /> + +
+ +
+
+
+
Suggested code
+
+ {suggestedSchema + ? isCodeExpanded + ? 'Expanded for review and copy/paste.' + : `Collapsed for mobile reading${codeLineCount > 4 ? ` · ${codeLineCount} lines available` : ''}.` + : 'No code snippet generated for this recommendation yet.'} +
+
+ {suggestedSchema && ( + toggleRecommendationCode(recommendation.id)} + /> + )} +
+ + {suggestedSchema ? ( + isCodeExpanded ? ( +
+                                      {suggestedSchema}
+                                    
+ ) : ( +
+ {codePreview} +
+ {codeLineCount > 4 ? `Show code to view ${codeLineCount - 4} more line${codeLineCount - 4 === 1 ? '' : 's'}.` : 'Show code to expand this snippet.'} +
+
+ ) + ) : ( +
+ No code snippet generated for this recommendation. +
+ )} +
+
+ ); + })}
))} @@ -1102,74 +1917,186 @@ https://example.com/pricing`} {activeResultsTab === 'delivery' && (
-
-

Delivery actions

-

- Export a developer handoff file, email the latest recommendations, or check Step 4 output for the selected platform. -

-
- - setEmailTo(event.target.value)} - /> - +
+
+
+

Delivery actions

+

+ Package the latest recommendations for your developer, email a handoff, or verify Step 4 output for the selected platform. +

+
+
+ + Mobile handoff +
+
+ +
+ {deliverySummaryCards.map((card) => ( + + ))}
- - { - handleExportAll().catch(() => null); - }} - /> - { - handleEmailCode().catch(() => null); - }} - /> - { - handlePlatformOutputCheck().catch(() => null); - }} - /> -
-
-
-
Selected Step 4 platform
-
{selectedPlatformLabel}
+
+
+ + {exportableRecommendations.length} code ready + + )} + > +
+
+ Best for quick sharing when you want one downloadable file instead of opening each recommendation card. +
+ { + handleExportAll().catch(() => null); + }} + /> +
+
+ + + {hasEmailRecipient ? 'Recipient ready' : 'Recipient needed'} + + )} + > +
+ + setEmailTo(event.target.value)} + /> + +
+ Use this when you want to send the full recommendation set straight from your phone. +
+ { + handleEmailCode().catch(() => null); + }} + /> +
+
+ + + {entitlements?.canPlatformOutput ? 'Premium unlocked' : 'Premium feature'} + + )} + > +
+
+
+
Selected platform
+
{selectedPlatformLabel}
+
+
+
Access
+
+ {entitlements?.canPlatformOutput ? 'Premium access detected' : 'Premium required for Step 4 output'} +
+
+
+ { + handlePlatformOutputCheck().catch(() => null); + }} + /> +
+
-
-
Access
-
- {entitlements?.canPlatformOutput ? 'Premium access detected' : 'Premium required for Step 4 output'} + +
+
+ + + +
+
Handoff checklist
+

+ A quick mobile-friendly checklist before you export or email the report. +

+
+
+ +
+ {deliveryChecklist.map((item) => ( +
+
+
+
{item.label}
+
{item.value}
+
+ + + {item.isReady ? 'Ready' : 'Needs attention'} + +
+
+ ))}
)}
- + +
)}