From 7fa9f5ed5f12b7404af86206efa047392c1e3c22 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 14 Apr 2026 20:21:21 +0000 Subject: [PATCH] Autosave: 20260414-202123 --- backend/src/services/sites.js | 5 +- frontend/src/pages/profile.tsx | 18 +- frontend/src/pages/sites/analyzer.tsx | 1734 ++++++++++++++++++++++--- 3 files changed, 1546 insertions(+), 211 deletions(-) diff --git a/backend/src/services/sites.js b/backend/src/services/sites.js index ac36a79..e1e4b55 100644 --- a/backend/src/services/sites.js +++ b/backend/src/services/sites.js @@ -12,7 +12,6 @@ const { getFirecrawlScaffold, crawlSiteWithFirecrawl } = require('./firecrawl'); const REQUEST_TIMEOUT = 15000; const PREVIEW_LIMIT = 5; -const PAGE_PREVIEW_LIMIT = 10; const NON_HTML_FILE_PATTERN = /\.(?:7z|avi|bmp|css|csv|docx?|eot|gif|ico|jpe?g|js|json|map|mov|mp3|mp4|pdf|png|pptx?|rar|svg|tar|tgz|txt|wav|webm|webp|woff2?|xlsx?|xml|zip)$/i; function normalizeUrl(rawUrl) { @@ -928,14 +927,14 @@ function buildAggregateAnalysis({ failedPages: failedPages.length, discoveredInternalPages, }, - pages: pageAnalyses.slice(0, PAGE_PREVIEW_LIMIT).map((page) => ({ + pages: pageAnalyses.map((page) => ({ url: page.analyzedUrl, title: page.pageTitle, statusCode: page.statusCode, hasStructuredData: Boolean(page.schema?.hasStructuredData), jsonLdTypes: page.schema?.jsonLd?.types || [], })), - failedPages: failedPages.slice(0, PAGE_PREVIEW_LIMIT), + failedPages, aggregateSignals, entitlements, firecrawl, diff --git a/frontend/src/pages/profile.tsx b/frontend/src/pages/profile.tsx index 000ed48..8c0d6b8 100644 --- a/frontend/src/pages/profile.tsx +++ b/frontend/src/pages/profile.tsx @@ -5,9 +5,7 @@ import { 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'; -import 'react-datepicker/dist/react-datepicker.css'; +import { toast } from 'react-toastify'; import CardBox from '../components/CardBox'; import LayoutAuthenticated from '../layouts/Authenticated'; @@ -20,21 +18,17 @@ import FormField from '../components/FormField'; import BaseDivider from '../components/BaseDivider'; import BaseButtons from '../components/BaseButtons'; import BaseButton from '../components/BaseButton'; -import FormCheckRadio from '../components/FormCheckRadio'; -import FormCheckRadioGroup from '../components/FormCheckRadioGroup'; import FormImagePicker from '../components/FormImagePicker'; import { SwitchField } from '../components/SwitchField'; import { SelectField } from '../components/SelectField'; -import { update, fetch } from '../stores/users/usersSlice'; +import { update } from '../stores/users/usersSlice'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { useRouter } from 'next/router'; -import {findMe} from "../stores/authSlice"; +import { findMe } from '../stores/authSlice'; const EditUsers = () => { - const { currentUser, isFetching, token } = useAppSelector( - (state) => state.auth, - ); + const { currentUser } = useAppSelector((state) => state.auth); const router = useRouter(); const dispatch = useAppDispatch(); const notify = (type, msg) => toast(msg, { type }); @@ -83,11 +77,11 @@ const EditUsers = () => { {''} - {currentUser?.avatar[0]?.publicUrl &&
+ {currentUser?.avatar?.[0]?.publicUrl &&
Avatar = { + wordpress: { + implementationLabel: 'JSON-LD handoff for theme or SEO plugin injection', + developerDestination: 'Theme hook, SEO plugin field, or reusable snippet partial', + payloadLabel: 'WordPress deployment package', + liveStatus: 'Live generator is still gated behind the Premium Step 4 workflow.', + demoNote: 'This demo shows the package shape a WordPress developer would receive, without publishing anything.', + iconPath: icon.mdiWordpress, + steps: [ + 'Bundle the schema-ready fixes into reusable JSON-LD blocks.', + 'Map each block to the matching WordPress template or page type.', + 'Hand off insertion notes for theme hooks or plugin fields.', + ], + }, + shopify: { + implementationLabel: 'Theme/app embed package for Shopify templates', + developerDestination: 'Theme section, app embed, or Liquid snippet placement', + payloadLabel: 'Shopify theme package', + liveStatus: 'Live generator is still gated behind the Premium Step 4 workflow.', + demoNote: 'This demo previews how a Shopify-focused package would be organized for implementation.', + iconPath: icon.mdiShopping, + steps: [ + 'Group schema output by product, collection, and content page patterns.', + 'Show where Liquid snippets or theme app blocks would be inserted.', + 'Package implementation notes for theme review before launch.', + ], + }, + webflow: { + implementationLabel: 'Embed/code-block package for Webflow pages and CMS templates', + developerDestination: 'Page settings, CMS template embeds, or shared custom code areas', + payloadLabel: 'Webflow embed package', + liveStatus: 'Live generator is still gated behind the Premium Step 4 workflow.', + demoNote: 'This demo mirrors the structure a Webflow implementer would review before copying embeds.', + iconPath: icon.mdiMonitorDashboard, + steps: [ + 'Split schema by static pages and CMS collection templates.', + 'Highlight where custom code or embed blocks would be added.', + 'Prepare a page-by-page install checklist for Webflow publishing.', + ], + }, + custom: { + implementationLabel: 'Developer handoff package for custom sites or other CMS platforms', + developerDestination: 'Template partials, component wrappers, or tag-manager-assisted deployment', + payloadLabel: 'Custom implementation package', + liveStatus: 'Live generator is still gated behind the Premium Step 4 workflow.', + demoNote: 'This demo keeps the output generic so your developer can adapt it to the stack in use.', + iconPath: icon.mdiCodeTags, + steps: [ + 'Bundle schema by priority so engineering can ship the highest-impact fixes first.', + 'Map each fix to the most likely template or component owner.', + 'Provide a simple validation checklist for QA after deployment.', + ], + }, +}; const parseTargetLines = (value: string) => value .split(/\r?\n/) @@ -405,6 +547,31 @@ const PageFilterChip = ({ label, count, iconPath, isActive, onClick }: PageFilte ); +const StatusBadge = ({ + children, + className = '', + iconPath, + iconSize = 14, + uppercase = false, + compact = false, + breakAll = false, + shadow = false, +}: StatusBadgeProps) => ( + + {iconPath && } + {children} + +); + const DeliverySummaryCard = ({ label, value, @@ -444,6 +611,122 @@ const DeliveryActionCard = ({ title, description, iconPath, badge, children }: D
); +const SectionStatCard = ({ + label, + value, + helper, + iconPath, + className = '', + labelClassName = '', + valueClassName = '', + helperClassName = '', + iconWrapperClassName = '', + iconSize = 18, +}: SectionStatCardProps) => ( +
+
+
+
+ {label} +
+
+ {value} +
+ {helper && ( +
+ {helper} +
+ )} +
+ {iconPath && ( + + + + )} +
+
+); + +const SummaryPanel = ({ + label, + value, + description, + aside, + children, + className = '', + labelClassName = '', + valueClassName = '', + descriptionClassName = '', +}: SummaryPanelProps) => ( +
+
+
+
+ {label} +
+ {value && ( +
+ {value} +
+ )} + {description && ( +
+ {description} +
+ )} +
+ {aside &&
{aside}
} +
+ {children &&
{children}
} +
+); + const SchemaAnalyzerPage = () => { const { currentUser } = useAppSelector((state) => state.auth); const [url, setUrl] = React.useState(''); @@ -452,12 +735,14 @@ const SchemaAnalyzerPage = () => { const [excludeTargets, setExcludeTargets] = React.useState(''); const [selectedPlatform, setSelectedPlatform] = React.useState('wordpress'); const [emailTo, setEmailTo] = React.useState(currentUser?.email || ''); - const [report, setReport] = React.useState(initialReport); + const [report, setReport] = React.useState(null); const [isAnalyzing, setIsAnalyzing] = React.useState(false); const [isExportingAll, setIsExportingAll] = React.useState(false); const [emailingId, setEmailingId] = React.useState(null); const [exportingId, setExportingId] = React.useState(null); const [isCheckingPlatformOutput, setIsCheckingPlatformOutput] = React.useState(false); + const [isSimulatingPlatformSend, setIsSimulatingPlatformSend] = React.useState(false); + const [lastPlatformSimulationAt, setLastPlatformSimulationAt] = React.useState(null); const [openSections, setOpenSections] = React.useState>({ targeting: false, options: false, @@ -480,6 +765,10 @@ const SchemaAnalyzerPage = () => { } }, [currentUser?.email]); + React.useEffect(() => { + setLastPlatformSimulationAt(null); + }, [selectedPlatform]); + React.useEffect(() => { if (!report?.analysis) { return undefined; @@ -490,6 +779,7 @@ const SchemaAnalyzerPage = () => { setActiveRecommendationFilter('all'); setIsFailedPagesExpanded(false); setExpandedRecommendationIds({}); + setLastPlatformSimulationAt(null); const timeoutId = window.setTimeout(() => { scrollToResults(); @@ -550,11 +840,27 @@ const SchemaAnalyzerPage = () => { const selectedPlatformLabel = PLATFORM_OPTIONS.find( (platformOption) => platformOption.value === selectedPlatform, )?.label || 'Custom / Other'; + const selectedPlatformMeta = PLATFORM_OUTPUT_META[selectedPlatform] || PLATFORM_OUTPUT_META.custom; 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 lastPlatformSimulationLabel = lastPlatformSimulationAt + ? new Date(lastPlatformSimulationAt).toLocaleString() + : null; + const trimmedEmailTo = emailTo.trim(); + const trimmedUrl = url.trim(); + const hasEmailRecipient = trimmedEmailTo.length > 0; + const hasUrl = trimmedUrl.length > 0; + const requestedPagesForRun = crawlPlan?.requestedPages || requestedPages; + const actualPagesAnalyzed = crawlPlan?.actualPagesAnalyzed || analyzedPages.length; + const failedPagesCount = report?.analysis?.crawlSummary?.failedPages ?? failedPages.length; + const requestedPageTargetMet = actualPagesAnalyzed >= requestedPagesForRun; + const pageCountStatusLabel = requestedPageTargetMet + ? `Exact ${requestedPagesForRun}-page target met` + : `Only ${actualPagesAnalyzed} eligible page${actualPagesAnalyzed === 1 ? ' was' : 's were'} found`; + const pageCountStatusDescription = requestedPageTargetMet + ? `This run analyzed all ${requestedPagesForRun} requested page${requestedPagesForRun === 1 ? '' : 's'}${failedPagesCount > 0 ? `, while ${failedPagesCount} additional fetch failure${failedPagesCount === 1 ? ' was' : 's were'} logged separately.` : '.'}` + : `You requested ${requestedPagesForRun} page${requestedPagesForRun === 1 ? '' : 's'}, but only ${actualPagesAnalyzed} crawlable page${actualPagesAnalyzed === 1 ? '' : 's'} matched the site and targeting rules for this run.`; const targetingSummary = hasTargetingRules ? `${appliedIncludeTargets.length} include · ${appliedExcludeTargets.length} exclude` : 'No targeting rules'; @@ -667,6 +973,530 @@ const SchemaAnalyzerPage = () => { : activePageFilter === 'missingSchema' ? 'No analyzed pages are missing structured data for this run.' : 'No page-level results are available yet for this analysis run.'; + const step4SchemaTypes = React.useMemo(() => Array.from(new Set([ + ...jsonLdTypes, + ...recommendations.map((recommendation) => recommendation.schema_type || '').filter(Boolean), + ])).slice(0, 8), [jsonLdTypes, recommendations]); + const step4PrimarySchemaType = step4SchemaTypes[0] || 'Organization'; + const step4PrimaryPageUrl = analyzedPages[0]?.url || report?.analysis?.analyzedUrl || report?.site?.base_url || trimmedUrl || 'https://example.com'; + const platformPreviewArtifacts = React.useMemo(() => { + const analyzedPageLabel = actualPagesAnalyzed === 1 ? '1 analyzed page' : `${actualPagesAnalyzed} analyzed pages`; + + if (selectedPlatform === 'wordpress') { + return [ + { + id: 'payload', + label: 'Schema package JSON', + fileName: 'schema-package.json', + description: `Contains the Step 4 payload for all ${analyzedPageLabel} in this run.`, + iconPath: icon.mdiCodeJson, + toneClassName: 'border-sky-200 bg-sky-50/70 dark:border-sky-500/30 dark:bg-sky-500/10', + }, + { + id: 'snippet', + label: 'Theme/plugin snippet', + fileName: 'wp-schema-snippet.php', + description: 'Ready for a theme hook, reusable partial, or an SEO plugin custom schema field.', + iconPath: icon.mdiWordpress, + }, + { + id: 'notes', + label: 'Install notes', + fileName: 'wordpress-install-notes.md', + description: 'Explains which templates or page types each schema block should be mapped to.', + iconPath: icon.mdiTextBoxCheckOutline, + }, + ]; + } + + if (selectedPlatform === 'shopify') { + return [ + { + id: 'payload', + label: 'Schema package JSON', + fileName: 'schema-package.json', + description: `Captures the exact ${analyzedPageLabel} selected for this Shopify handoff.`, + iconPath: icon.mdiCodeJson, + toneClassName: 'border-violet-200 bg-violet-50/70 dark:border-violet-500/30 dark:bg-violet-500/10', + }, + { + id: 'snippet', + label: 'Liquid snippet', + fileName: 'snippets/schema-output.liquid', + description: 'Shows how product, collection, or content templates could render JSON-LD in Liquid.', + iconPath: icon.mdiShopping, + }, + { + id: 'notes', + label: 'Theme checklist', + fileName: 'shopify-theme-checklist.md', + description: 'Documents the theme sections or app embed locations your developer should review.', + iconPath: icon.mdiClipboardCheckOutline, + }, + ]; + } + + if (selectedPlatform === 'webflow') { + return [ + { + id: 'payload', + label: 'Schema package JSON', + fileName: 'schema-package.json', + description: `Preserves all ${analyzedPageLabel} so the Webflow handoff matches the crawl exactly.`, + iconPath: icon.mdiCodeJson, + toneClassName: 'border-emerald-200 bg-emerald-50/70 dark:border-emerald-500/30 dark:bg-emerald-500/10', + }, + { + id: 'snippet', + label: 'Embed snippet', + fileName: 'webflow-embed-snippet.html', + description: 'Represents the custom code block that would be pasted into page settings or CMS templates.', + iconPath: icon.mdiMonitorDashboard, + }, + { + id: 'notes', + label: 'Publish checklist', + fileName: 'webflow-publish-checklist.md', + description: 'Calls out which static pages or CMS collections need a schema embed before publish.', + iconPath: icon.mdiClipboardPulseOutline, + }, + ]; + } + + return [ + { + id: 'payload', + label: 'Schema package JSON', + fileName: 'schema-package.json', + description: `Keeps the full ${analyzedPageLabel} package available for engineering review.`, + iconPath: icon.mdiCodeJson, + toneClassName: 'border-amber-200 bg-amber-50/70 dark:border-amber-500/30 dark:bg-amber-500/10', + }, + { + id: 'snippet', + label: 'Implementation starter', + fileName: 'schema-loader.ts', + description: 'A generic starter that engineering can adapt to the current frontend, backend, or CMS stack.', + iconPath: icon.mdiCodeTags, + }, + { + id: 'notes', + label: 'Developer notes', + fileName: 'implementation-notes.md', + description: 'Summarizes target templates, QA checks, and rollout notes for a custom implementation.', + iconPath: icon.mdiNotebookOutline, + }, + ]; + }, [actualPagesAnalyzed, selectedPlatform]); + const platformFinalDeliverables = React.useMemo(() => { + if (selectedPlatform === 'wordpress') { + return [ + { + id: 'plugin-field', + title: 'SEO plugin field', + destination: 'Yoast / Rank Math / custom schema field', + owner: 'SEO owner', + description: 'Best for singular pages where a marketer or SEO specialist can manage the final JSON-LD without editing templates.', + iconPath: icon.mdiWordpress, + statusLabel: 'Ready for config', + toneClassName: 'border-sky-200 bg-sky-50/70 dark:border-sky-500/30 dark:bg-sky-500/10', + }, + { + id: 'theme-hook', + title: 'Theme hook snippet', + destination: 'wp_head hook or reusable template partial', + owner: 'Frontend developer', + description: 'Used when the schema package needs template-aware logic across posts, pages, or custom content types.', + iconPath: icon.mdiCodeBraces, + statusLabel: 'Ready for theme dev', + }, + { + id: 'validation', + title: 'Validation checklist', + destination: 'Rich Results + source-code spot checks', + owner: 'QA reviewer', + description: 'Confirms the packaged schema renders on every analyzed page before the team marks the release complete.', + iconPath: icon.mdiClipboardCheckOutline, + statusLabel: 'Ready for QA', + }, + ]; + } + + if (selectedPlatform === 'shopify') { + return [ + { + id: 'liquid-snippet', + title: 'Liquid theme snippet', + destination: 'snippets/schema-output.liquid', + owner: 'Theme developer', + description: 'Delivers the generated JSON-LD into product, collection, or article templates with Liquid-aware variables.', + iconPath: icon.mdiShopping, + statusLabel: 'Ready for theme merge', + toneClassName: 'border-violet-200 bg-violet-50/70 dark:border-violet-500/30 dark:bg-violet-500/10', + }, + { + id: 'app-embed', + title: 'Theme app/embed slot', + destination: 'App embed block or section-level insertion point', + owner: 'Merchant ops', + description: 'Shows where a merchant or implementation partner would enable the package inside the active theme.', + iconPath: icon.mdiPackageVariantClosed, + statusLabel: 'Ready for enablement', + }, + { + id: 'validation', + title: 'Storefront validation', + destination: 'Preview theme + rich result validation', + owner: 'QA reviewer', + description: 'Validates the schema output across the selected storefront pages before the theme is published live.', + iconPath: icon.mdiClipboardCheckOutline, + statusLabel: 'Ready for QA', + }, + ]; + } + + if (selectedPlatform === 'webflow') { + return [ + { + id: 'page-embed', + title: 'Page embed block', + destination: 'Page settings custom code or embed element', + owner: 'Webflow editor', + description: 'Ideal for static pages that need a fast JSON-LD insert without changing the rest of the layout.', + iconPath: icon.mdiMonitorDashboard, + statusLabel: 'Ready for page paste', + toneClassName: 'border-emerald-200 bg-emerald-50/70 dark:border-emerald-500/30 dark:bg-emerald-500/10', + }, + { + id: 'cms-template', + title: 'CMS template mapping', + destination: 'Collection template custom code region', + owner: 'CMS implementer', + description: 'Maps the generated package to Webflow CMS templates so repeatable schema can be applied at scale.', + iconPath: icon.mdiCodeBraces, + statusLabel: 'Ready for template wiring', + }, + { + id: 'publish-review', + title: 'Publish review', + destination: 'Pre-publish checklist and live spot check', + owner: 'Publisher', + description: 'Adds a final review pass before Webflow publish so every analyzed page gets its expected schema block.', + iconPath: icon.mdiClipboardPulseOutline, + statusLabel: 'Ready for publish review', + }, + ]; + } + + return [ + { + id: 'component-slot', + title: 'Component integration slot', + destination: 'Shared layout, component wrapper, or middleware hook', + owner: 'Engineering', + description: 'Provides the most flexible handoff for React, Node, headless CMS, or other custom delivery stacks.', + iconPath: icon.mdiCodeTags, + statusLabel: 'Ready for engineering', + toneClassName: 'border-amber-200 bg-amber-50/70 dark:border-amber-500/30 dark:bg-amber-500/10', + }, + { + id: 'deployment-task', + title: 'Deployment task', + destination: 'Release checklist or ticketed implementation step', + owner: 'Project manager', + description: 'Turns the Step 4 package into a concrete delivery task that engineering can estimate and schedule.', + iconPath: icon.mdiTrayArrowUp, + statusLabel: 'Ready for planning', + }, + { + id: 'validation', + title: 'Post-release validation', + destination: 'QA checklist and monitor pass', + owner: 'QA reviewer', + description: 'Captures the checks needed after deployment so the team can confirm production pages match the package.', + iconPath: icon.mdiClipboardCheckOutline, + statusLabel: 'Ready for sign-off', + }, + ]; + }, [selectedPlatform]); + const platformPageMappings = React.useMemo(() => { + const implementationArtifacts = platformPreviewArtifacts.filter((artifact) => artifact.id !== 'payload'); + + return analyzedPages.map((page, index) => { + const mappedDeliverable = platformFinalDeliverables[index % platformFinalDeliverables.length] || platformFinalDeliverables[0]; + const mappedArtifact = implementationArtifacts[index % implementationArtifacts.length] || platformPreviewArtifacts[0]; + const schemaType = page.jsonLdTypes?.[0] || step4SchemaTypes[index % step4SchemaTypes.length] || step4PrimarySchemaType; + const hasExistingStructuredData = Boolean(page.hasStructuredData); + + return { + id: page.url || `${page.title || 'page'}-${index}`, + pageLabel: page.title || page.url || `Page ${index + 1}`, + pageUrl: page.url || null, + schemaType, + packageFile: mappedArtifact?.fileName || platformPreviewArtifacts[0]?.fileName || selectedPlatformMeta.payloadLabel, + deliverableTitle: mappedDeliverable?.title || selectedPlatformMeta.payloadLabel, + destination: mappedDeliverable?.destination || selectedPlatformMeta.developerDestination, + actionLabel: hasExistingStructuredData + ? 'Review the existing markup and merge only the missing schema improvements.' + : 'Inject the generated schema output for this page during implementation.', + statusLabel: hasExistingStructuredData ? 'Existing markup detected' : 'Schema output pending', + statusClassName: hasExistingStructuredData + ? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-200' + : 'bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-200', + iconPath: hasExistingStructuredData ? icon.mdiCheckCircleOutline : icon.mdiAlertCircleOutline, + }; + }); + }, [ + analyzedPages, + platformFinalDeliverables, + platformPreviewArtifacts, + selectedPlatformMeta.developerDestination, + selectedPlatformMeta.payloadLabel, + step4PrimarySchemaType, + step4SchemaTypes, + ]); + const platformPublishTimeline = React.useMemo(() => { + const baseTimestamp = lastPlatformSimulationAt ? new Date(lastPlatformSimulationAt) : null; + const formatStepTime = (minuteOffset: number, fallback: string) => { + if (!baseTimestamp) { + return fallback; + } + + return new Date(baseTimestamp.getTime() + (minuteOffset * 60 * 1000)).toLocaleTimeString([], { + hour: 'numeric', + minute: '2-digit', + }); + }; + + if (!baseTimestamp) { + return [ + { + id: 'queued', + label: 'Queued', + description: `${selectedPlatformLabel} handoff is ready to stage as soon as you run the demo send.`, + iconPath: icon.mdiClockOutline, + status: 'current', + timeLabel: 'Waiting for demo run', + }, + { + id: 'packaged', + label: 'Packaged', + description: 'Package files, page mapping, and payload JSON will be assembled for review.', + iconPath: icon.mdiPackageVariantClosed, + status: 'upcoming', + timeLabel: 'Pending', + }, + { + id: 'ready-for-dev', + label: 'Ready for dev', + description: 'The implementation handoff will be marked ready for a developer or implementation partner.', + iconPath: icon.mdiTrayArrowUp, + status: 'upcoming', + timeLabel: 'Pending', + }, + { + id: 'approved', + label: 'Approved', + description: 'Stakeholder approval appears here after the Step 4 demo handoff is reviewed.', + iconPath: icon.mdiThumbUpOutline, + status: 'upcoming', + timeLabel: 'Pending', + }, + ]; + } + + return [ + { + id: 'queued', + label: 'Queued', + description: `${selectedPlatformLabel} demo package entered the delivery queue.`, + iconPath: icon.mdiClockOutline, + status: 'complete', + timeLabel: formatStepTime(0, 'Queued'), + }, + { + id: 'packaged', + label: 'Packaged', + description: 'Files, payload preview, and page mapping were bundled into the demo handoff.', + iconPath: icon.mdiPackageVariantClosed, + status: 'complete', + timeLabel: formatStepTime(1, 'Packaged'), + }, + { + id: 'ready-for-dev', + label: 'Ready for dev', + description: 'The handoff was marked ready for engineering review and platform implementation.', + iconPath: icon.mdiTrayArrowUp, + status: 'complete', + timeLabel: formatStepTime(3, 'Ready'), + }, + { + id: 'approved', + label: 'Approved', + description: 'The preview package reached the final stakeholder-approved demo state.', + iconPath: icon.mdiThumbUpOutline, + status: 'current', + timeLabel: formatStepTime(6, 'Approved'), + }, + ]; + }, [lastPlatformSimulationAt, selectedPlatformLabel]); + const platformImplementationPreview = React.useMemo(() => { + if (selectedPlatform === 'wordpress') { + return ` + + + { + "@context": "https://schema.org", + "@type": "${step4PrimarySchemaType}", + "url": "{{ shop.url }}{{ request.path }}" + } + +{% endif %}`; + } + + if (selectedPlatform === 'webflow') { + return ``; + } + + return `export const schemaPayload = { + "@context": "https://schema.org", + "@type": "${step4PrimarySchemaType}", + url: "${step4PrimaryPageUrl}", +};`; + }, [selectedPlatform, step4PrimaryPageUrl, step4PrimarySchemaType]); + const platformOutputPreviewPayload = React.useMemo(() => ({ + demo: true, + livePublish: false, + platform: { + key: selectedPlatform, + label: selectedPlatformLabel, + packageType: selectedPlatformMeta.payloadLabel, + implementation: selectedPlatformMeta.implementationLabel, + destination: selectedPlatformMeta.developerDestination, + }, + site: { + id: report?.site?.id || null, + name: report?.site?.name || null, + baseUrl: report?.site?.base_url || trimmedUrl || null, + detectedPlatform: report?.analysis?.platform?.label || 'Unknown', + }, + analysis: { + analyzedAt: report?.analysis?.fetchedAt || new Date().toISOString(), + requestedPages: requestedPagesForRun, + actualPagesAnalyzed, + failedPages: failedPagesCount, + exactRequestedPageTargetMet: requestedPageTargetMet, + structuredDataTypes: step4SchemaTypes, + notice: report?.analysis?.notice || null, + }, + pages: analyzedPages.map((page) => ({ + url: page.url || null, + title: page.title || null, + statusCode: page.statusCode || null, + hasStructuredData: Boolean(page.hasStructuredData), + jsonLdTypes: page.jsonLdTypes || [], + })), + finalDeliverables: platformFinalDeliverables.map((deliverable) => ({ + id: deliverable.id, + title: deliverable.title, + owner: deliverable.owner, + destination: deliverable.destination, + status: deliverable.statusLabel, + })), + pageMappings: platformPageMappings.map((mapping) => ({ + page: mapping.pageLabel, + url: mapping.pageUrl, + schemaType: mapping.schemaType, + deliverable: mapping.deliverableTitle, + destination: mapping.destination, + packageFile: mapping.packageFile, + action: mapping.actionLabel, + status: mapping.statusLabel, + })), + timeline: platformPublishTimeline.map((step) => ({ + id: step.id, + label: step.label, + status: step.status, + timeLabel: step.timeLabel, + })), + recommendations: exportableRecommendations.map((recommendation) => ({ + id: recommendation.id, + title: recommendation.title, + priority: recommendation.priority || 'n/a', + scope: recommendation.page_scope || 'n/a', + schemaType: recommendation.schema_type || 'Schema', + })), + }), [ + actualPagesAnalyzed, + analyzedPages, + exportableRecommendations, + failedPagesCount, + platformFinalDeliverables, + platformPageMappings, + platformPublishTimeline, + report?.analysis?.fetchedAt, + report?.analysis?.notice, + report?.analysis?.platform?.label, + report?.site?.base_url, + report?.site?.id, + report?.site?.name, + requestedPageTargetMet, + requestedPagesForRun, + selectedPlatform, + selectedPlatformLabel, + selectedPlatformMeta.developerDestination, + selectedPlatformMeta.implementationLabel, + selectedPlatformMeta.payloadLabel, + step4SchemaTypes, + trimmedUrl, + ]); + const platformOutputPreviewJson = React.useMemo( + () => JSON.stringify(platformOutputPreviewPayload, null, 2), + [platformOutputPreviewPayload], + ); + const platformHandoffText = React.useMemo(() => [ + `Step 4 demo handoff for ${selectedPlatformLabel}`, + `Site: ${report?.site?.base_url || trimmedUrl || 'Not set'}`, + `Requested pages: ${requestedPagesForRun}`, + `Pages analyzed: ${actualPagesAnalyzed}`, + `Platform package: ${selectedPlatformMeta.payloadLabel}`, + `Implementation target: ${selectedPlatformMeta.developerDestination}`, + step4SchemaTypes.length > 0 ? `Schema types: ${step4SchemaTypes.join(', ')}` : 'Schema types: To be determined from recommendations', + '', + 'Demo workflow preview:', + ...selectedPlatformMeta.steps.map((step, index) => `${index + 1}. ${step}`), + ].filter(Boolean).join('\n'), [ + actualPagesAnalyzed, + report?.site?.base_url, + requestedPagesForRun, + selectedPlatformLabel, + selectedPlatformMeta.developerDestination, + selectedPlatformMeta.payloadLabel, + selectedPlatformMeta.steps, + step4SchemaTypes, + trimmedUrl, + ]); const deliverySummaryCards = [ { label: 'Code-ready fixes', @@ -682,7 +1512,7 @@ const SchemaAnalyzerPage = () => { { label: 'Email recipient', value: hasEmailRecipient ? 'Ready' : 'Missing', - helper: hasEmailRecipient ? emailTo.trim() : 'Add a developer email before sending the handoff.', + helper: hasEmailRecipient ? trimmedEmailTo : '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' @@ -704,7 +1534,7 @@ const SchemaAnalyzerPage = () => { { id: 'recipient', label: 'Recipient email', - value: hasEmailRecipient ? emailTo.trim() : 'Add an email to send the handoff.', + value: hasEmailRecipient ? trimmedEmailTo : 'Add an email to send the handoff.', isReady: hasEmailRecipient, }, { @@ -724,6 +1554,30 @@ const SchemaAnalyzerPage = () => { isReady: Boolean(entitlements?.canPlatformOutput), }, ]; + const siteSnapshotStats = [ + { + label: 'Platform', + value: report?.analysis?.platform?.label || 'Unknown platform', + }, + { + label: 'Last updated', + value: analyzedTimestamp || 'Just now', + }, + ]; + const planDetailStats = [ + { + label: 'Access level', + value: entitlements?.canPlatformOutput ? 'Premium' : 'Basic', + }, + { + label: 'Pages allowed', + value: `Up to ${maxPagesPerCrawl} pages per crawl`, + }, + { + label: 'Step 4 output', + value: entitlements?.canPlatformOutput ? 'Available to check' : 'Premium only', + }, + ]; const overviewStats = [ { label: 'Pages analyzed', @@ -762,6 +1616,36 @@ const SchemaAnalyzerPage = () => { iconPath: icon.mdiAlertOutline, }, ]; + const crawlPlanStats = [ + { + label: 'Requested', + value: crawlPlan?.requestedPages || 1, + }, + { + label: 'Plan limit', + value: crawlPlan?.allowedPages || maxPagesPerCrawl, + }, + { + label: 'Analyzed', + value: crawlPlan?.actualPagesAnalyzed || 0, + }, + { + label: 'Failed', + value: report?.analysis?.crawlSummary?.failedPages ?? failedPages.length, + }, + ]; + const crawlSummaryStats = report?.analysis?.crawlSummary + ? [ + { + label: 'Pages without structured data', + value: report.analysis.crawlSummary.pagesWithoutStructuredData ?? 0, + }, + { + label: 'Discovered internal pages', + value: report.analysis.crawlSummary.discoveredInternalPages ?? analyzedPages.length, + }, + ] + : []; React.useEffect(() => { if ((draftIncludeTargets.length > 0 || draftExcludeTargets.length > 0) && !openSections.targeting) { @@ -793,7 +1677,7 @@ const SchemaAnalyzerPage = () => { }; const handleAnalyze = async () => { - if (!url.trim()) { + if (!trimmedUrl) { notify('error', 'Enter a website URL first.'); return; } @@ -809,7 +1693,7 @@ const SchemaAnalyzerPage = () => { try { setIsAnalyzing(true); const response = await axios.post('/sites/analyze', { - url: url.trim(), + url: trimmedUrl, requestedPages, includeTargets, excludeTargets, @@ -846,6 +1730,39 @@ const SchemaAnalyzerPage = () => { } }; + const handleCopyText = async (value: string, successMessage: string) => { + try { + await navigator.clipboard.writeText(value); + notify('success', successMessage); + } catch (error) { + console.error('Copy text failed:', error); + notify('error', 'Unable to copy text in this browser.'); + } + }; + + const handleSimulatePlatformSend = async () => { + if (!report?.site?.id) { + notify('error', 'Analyze a site first.'); + return; + } + + try { + setIsSimulatingPlatformSend(true); + await new Promise((resolve) => { + window.setTimeout(resolve, 900); + }); + + const simulatedAt = new Date().toISOString(); + setLastPlatformSimulationAt(simulatedAt); + notify('success', `Demo output prepared for ${selectedPlatformLabel}. No live publish was performed.`); + } catch (error) { + console.error('Simulate Step 4 send failed:', error); + notify('error', 'Unable to simulate Step 4 output right now.'); + } finally { + setIsSimulatingPlatformSend(false); + } + }; + const downloadBlob = (blob: Blob, filename: string) => { const blobUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); @@ -919,7 +1836,7 @@ const SchemaAnalyzerPage = () => { return; } - if (!emailTo.trim()) { + if (!trimmedEmailTo) { notify('error', 'Add a recipient email first.'); return; } @@ -929,11 +1846,11 @@ const SchemaAnalyzerPage = () => { await axios.post('/sites/email-code', recommendationId ? { recommendationId, - to: emailTo.trim(), + to: trimmedEmailTo, } : { siteId: report.site.id, - to: emailTo.trim(), + to: trimmedEmailTo, }); notify('success', recommendationId ? 'Schema code emailed.' : 'Full recommendation report emailed.'); } catch (error: any) { @@ -1012,7 +1929,7 @@ const SchemaAnalyzerPage = () => { onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); - handleAnalyze().catch(() => null); + void handleAnalyze(); } }} /> @@ -1115,12 +2032,13 @@ https://example.com/pricing`}
Include targets
{appliedIncludeTargets.map((target) => ( - {target} - + ))}
@@ -1131,12 +2049,13 @@ https://example.com/pricing`}
Exclude targets
{appliedExcludeTargets.map((target) => ( - {target} - + ))}
@@ -1188,7 +2107,7 @@ https://example.com/pricing`} className='w-full justify-center px-4 py-3 text-sm shadow-sm sm:w-auto' disabled={!hasUrl || isAnalyzing || isRequestedPagesOverLimit} onClick={() => { - handleAnalyze().catch(() => null); + void handleAnalyze(); }} /> @@ -1235,13 +2154,14 @@ https://example.com/pricing`} -
-
Selected output target
-
{selectedPlatformLabel}
-

- Keep this aligned with the CMS or platform your developer will implement against. -

-
+ @@ -1254,24 +2174,16 @@ https://example.com/pricing`} onToggle={() => toggleSection('limits')} >
-
-
Access level
-
- {entitlements?.canPlatformOutput ? 'Premium' : 'Basic'} -
-
-
-
Pages allowed
-
- Up to {maxPagesPerCrawl} pages per crawl -
-
-
-
Step 4 output
-
- {entitlements?.canPlatformOutput ? 'Available to check' : 'Premium only'} -
-
+ {planDetailStats.map((stat) => ( + + ))}
@@ -1282,7 +2194,7 @@ https://example.com/pricing`}
- {hasUrl ? url.trim() : 'Enter a website URL'} + {hasUrl ? trimmedUrl : 'Enter a website URL'}
{requestedPages} page{requestedPages === 1 ? '' : 's'} requested · {targetingSummary} @@ -1295,7 +2207,7 @@ https://example.com/pricing`} className='shrink-0 justify-center px-4 py-3 text-sm shadow-sm' disabled={!hasUrl || isAnalyzing || isRequestedPagesOverLimit} onClick={() => { - handleAnalyze().catch(() => null); + void handleAnalyze(); }} />
@@ -1312,13 +2224,16 @@ https://example.com/pricing`}

- + {report.analysis.platform?.label || 'Unknown platform'} - + {analyzedTimestamp && ( - + {analyzedTimestamp} - + )}
@@ -1375,40 +2290,28 @@ https://example.com/pricing`}
-
-
Platform
-
- {report.analysis.platform?.label || 'Unknown platform'} -
-
-
-
Last updated
-
- {analyzedTimestamp || 'Just now'} -
-
+ {siteSnapshotStats.map((stat) => ( + + ))}
{overviewStats.map((stat) => ( -
-
-
-
{stat.label}
-
- {stat.value} -
-
{stat.helper}
-
- - - -
-
+ label={stat.label} + value={stat.value} + helper={stat.helper} + iconPath={stat.iconPath} + className='p-3.5 sm:p-4' + /> ))}
@@ -1422,22 +2325,15 @@ https://example.com/pricing`}
-
-
Requested
-
{crawlPlan.requestedPages || 1}
-
-
-
Plan limit
-
{crawlPlan.allowedPages || maxPagesPerCrawl}
-
-
-
Analyzed
-
{crawlPlan.actualPagesAnalyzed || 0}
-
-
-
Failed
-
{report.analysis.crawlSummary?.failedPages ?? failedPages.length}
-
+ {crawlPlanStats.map((stat) => ( + + ))}
{report.analysis.notice &&
{report.analysis.notice}
} @@ -1445,18 +2341,15 @@ https://example.com/pricing`} {report.analysis.crawlSummary && (
-
-
Pages without structured data
-
- {report.analysis.crawlSummary.pagesWithoutStructuredData ?? 0} -
-
-
-
Discovered internal pages
-
- {report.analysis.crawlSummary.discoveredInternalPages ?? analyzedPages.length} -
-
+ {crawlSummaryStats.map((stat) => ( + + ))}
)} @@ -1469,12 +2362,12 @@ https://example.com/pricing`}
Included
{appliedIncludeTargets.map((target) => ( - {target} - + ))}
@@ -1484,12 +2377,12 @@ https://example.com/pricing`}
Excluded
{appliedExcludeTargets.map((target) => ( - {target} - + ))}
@@ -1503,12 +2396,12 @@ https://example.com/pricing`}
Detected JSON-LD types
{jsonLdTypes.map((typeName) => ( - {typeName} - + ))}
@@ -1608,43 +2501,48 @@ https://example.com/pricing`}
- + 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.
+
No JSON-LD types were detected on this page.
)} -
+
))} @@ -1770,9 +2668,13 @@ https://example.com/pricing`} - + {recommendationGroup.recommendations.length} item{recommendationGroup.recommendations.length === 1 ? '' : 's'} - + @@ -1792,24 +2694,26 @@ https://example.com/pricing`}
- + {priorityMeta.label} - + {recommendation.schema_type && ( - + {recommendation.schema_type} - + )} {recommendation.page_scope && ( - + {recommendation.page_scope} - + )} - + {recommendation.suggested_schema ? 'Code ready' : 'Needs code'} - +

{recommendation.title} @@ -1834,7 +2738,7 @@ https://example.com/pricing`} className='w-full justify-center sm:w-auto' disabled={!recommendation.suggested_schema} onClick={() => { - handleCopyCode(recommendation).catch(() => null); + void handleCopyCode(recommendation); }} /> { - handleExportRecommendation(recommendation).catch(() => null); + void handleExportRecommendation(recommendation); }} /> { - handleEmailCode(recommendation.id).catch(() => null); + void handleEmailCode(recommendation.id); }} /> @@ -1925,10 +2829,14 @@ https://example.com/pricing`} Package the latest recommendations for your developer, email a handoff, or verify Step 4 output for the selected platform.

-
- - Mobile handoff -
+ + Mobile handoff +
@@ -1952,9 +2860,9 @@ https://example.com/pricing`} description='Download a single package with the latest recommendations so it is easy to forward or attach in your workflow.' iconPath={icon.mdiDownload} badge={( - + {exportableRecommendations.length} code ready - + )} >
@@ -1968,7 +2876,7 @@ https://example.com/pricing`} className='w-full justify-center' disabled={!report?.site?.id || isExportingAll} onClick={() => { - handleExportAll().catch(() => null); + void handleExportAll(); }} />
@@ -1979,11 +2887,13 @@ https://example.com/pricing`} description='Send the current recommendations directly to a developer or implementation partner without leaving the analyzer.' iconPath={icon.mdiEmailOutline} badge={( - + {hasEmailRecipient ? 'Recipient ready' : 'Recipient needed'} - + )} >
@@ -2006,7 +2916,7 @@ https://example.com/pricing`} className='w-full justify-center' disabled={!report?.site?.id || emailingId === 'all'} onClick={() => { - handleEmailCode().catch(() => null); + void handleEmailCode(); }} />
@@ -2017,25 +2927,25 @@ https://example.com/pricing`} description='Verify whether platform-specific implementation output is available for the platform selected in setup.' iconPath={entitlements?.canPlatformOutput ? icon.mdiCodeBraces : icon.mdiLockOutline} badge={( - + {entitlements?.canPlatformOutput ? 'Premium unlocked' : 'Premium feature'} - + )} >
-
-
Selected platform
-
{selectedPlatformLabel}
-
-
-
Access
-
- {entitlements?.canPlatformOutput ? 'Premium access detected' : 'Premium required for Step 4 output'} -
-
+ +
{ - handlePlatformOutputCheck().catch(() => null); + void handlePlatformOutputCheck(); }} />
@@ -2059,7 +2969,7 @@ https://example.com/pricing`}
- +
Handoff checklist
@@ -2071,27 +2981,459 @@ https://example.com/pricing`}
{deliveryChecklist.map((item) => ( -
-
-
-
{item.label}
-
{item.value}
-
- - - {item.isReady ? 'Ready' : 'Needs attention'} - -
-
+ label={item.label} + description={item.value} + aside={( + + {item.isReady ? 'Ready' : 'Needs attention'} + + )} + /> ))}
+ +
+
+
+
Step 4 MVP / demo preview
+

+ This is a frontend demo of what the selected platform handoff could look like. It uses the current report data, keeps the exact requested page target visible, and never publishes to a live platform. +

+
+
+ + {selectedPlatformLabel} + + + {requestedPageTargetMet ? 'Exact page target met' : 'Limited by crawlable pages'} + + + Demo only + +
+
+ +
+ + + +
+ +
+
+ + {selectedPlatformMeta.payloadLabel} + + )} + > +
+ {selectedPlatformMeta.steps.map((step, index) => ( +
+ + {index + 1} + + {step} +
+ ))} +
+
+ + +
+ {platformFinalDeliverables.map((deliverable) => ( +
+
+
+
+ {deliverable.title} +
+
+ {deliverable.destination} +
+
+ + + +
+
+ + {deliverable.owner} + + + {deliverable.statusLabel} + +
+

+ {deliverable.description} +

+
+ ))} +
+
+ + +
+ {platformPreviewArtifacts.map((artifact) => ( +
+
+
+
+ {artifact.label} +
+
+ {artifact.fileName} +
+
+ + + +
+

+ {artifact.description} +

+
+ ))} +
+
+ + +
+
+ {selectedPlatformMeta.implementationLabel} +
+
+                              {platformImplementationPreview}
+                            
+
+
+ + +
+
+ {selectedPlatformMeta.payloadLabel} +
+
+                              {platformOutputPreviewJson}
+                            
+
+
+ + + { + void handleCopyText(platformOutputPreviewJson, 'Step 4 demo payload copied.'); + }} + /> + { + void handleCopyText(platformHandoffText, 'Step 4 handoff notes copied.'); + }} + /> + { + void handleSimulatePlatformSend(); + }} + /> + +
+ +
+ + {actualPagesAnalyzed}/{requestedPagesForRun} + + )} + /> + + + + 0 + ? `${step4SchemaTypes.length} schema type${step4SchemaTypes.length === 1 ? '' : 's'} will be highlighted in the developer handoff.` + : 'Schema types will populate here as soon as the crawl identifies code-ready opportunities.'} + > +
+ {step4SchemaTypes.length > 0 ? step4SchemaTypes.map((typeName) => ( + + {typeName} + + )) : ( + + No schema types yet + + )} +
+
+ + 0 + ? `All ${actualPagesAnalyzed} analyzed page${actualPagesAnalyzed === 1 ? '' : 's'} are mapped to a schema type, package file, and delivery target in this demo.` + : 'Run an analysis to populate the Step 4 package preview.'} + > +
+ {platformPageMappings.length > 0 ? platformPageMappings.map((mapping) => ( +
+
+
+
+ {mapping.pageLabel} +
+ {mapping.pageUrl && ( +
+ {mapping.pageUrl} +
+ )} +
+ + {mapping.statusLabel} + +
+
+ + {mapping.schemaType} + + + {mapping.packageFile} + +
+
+
+
+ Deliverable +
+
+ {mapping.deliverableTitle} +
+
+ {mapping.destination} +
+
+
+
+ Next action +
+
+ {mapping.actionLabel} +
+
+
+
+ )) : ( +
+ No analyzed pages yet. Run the analyzer first to build a Step 4 preview package. +
+ )} +
+
+ + + {lastPlatformSimulationLabel ? 'Demo staged' : 'Awaiting demo'} + + )} + > +
+ {platformPublishTimeline.map((step, index) => ( +
+
+
+ + + +
+
+ + {index + 1}. {step.label} + + + {step.status === 'complete' ? 'Complete' : step.status === 'current' ? 'Current' : 'Upcoming'} + +
+

+ {step.description} +

+
+
+
+ {step.timeLabel} +
+
+
+ ))} +
+
+ + {lastPlatformSimulationLabel && ( + + Demo ready + + )} + /> + )} +
+
+
)}