diff --git a/src/components/headline-tree.tsx b/src/components/headline-tree.tsx index 1245e12..b671509 100644 --- a/src/components/headline-tree.tsx +++ b/src/components/headline-tree.tsx @@ -4,7 +4,12 @@ import { useState } from "react"; import { Badge } from "@/components/ui/badge"; import type { HeadlineNode } from "@/app/actions"; import { KeywordHighlighter } from "./keyword-highlighter"; -import { ChevronDown, ChevronsUp, ChevronsDown } from "lucide-react"; +import { + ChevronDown, + ChevronsUp, + ChevronsDown, + AlertTriangle, +} from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { @@ -36,6 +41,13 @@ const getCollapsiblePaths = ( return paths; }; +const HierarchyWarning = ({ message }: { message: string }) => ( +
+ +

{message}

+
+); + const HeadlineNodeDisplay = ({ node, level, @@ -43,49 +55,79 @@ const HeadlineNodeDisplay = ({ keyword, collapsedStates, onToggle, + parentLevel, + isDuplicateH1, }: { node: HeadlineNode; level: number; - path: string; + path:string; keyword?: string | null; collapsedStates: Record; onToggle: (path: string) => void; + parentLevel: number; + isDuplicateH1: boolean; }) => { const isCollapsible = node.level === 2 && node.children && node.children.length > 0; const isCollapsed = collapsedStates[path]; + const levelSkipped = parentLevel > 0 && node.level > parentLevel + 1; + const hasError = isDuplicateH1 || levelSkipped; + + let errorTooltip: string | null = null; + if (isDuplicateH1) { + errorTooltip = "Duplicate H1 tag. Only one H1 is recommended per page."; + } else if (levelSkipped) { + errorTooltip = `Incorrect hierarchy. An H${node.level} should not directly follow an H${parentLevel}.`; + } + + const content = ( +
isCollapsible && onToggle(path)} + > + + {node.tag} + +

+ +

+

+ {node.length} +

+ {isCollapsible && ( + + )} +
+ ); + return ( <> -
isCollapsible && onToggle(path)} - > - - {node.tag} - -

- -

-

- {node.length} -

- {isCollapsible && ( - - )} -
+ {errorTooltip ? ( + + + {content} + +

{errorTooltip}

+
+
+
+ ) : ( + content + )} {node.children && (!isCollapsible || !isCollapsed) && node.children.map((child, index) => ( @@ -97,6 +139,8 @@ const HeadlineNodeDisplay = ({ keyword={keyword} collapsedStates={collapsedStates} onToggle={onToggle} + parentLevel={node.level} + isDuplicateH1={false} /> ))} @@ -139,9 +183,17 @@ export function HeadlineTree({ headlines, keyword }: HeadlineTreeProps) { }; const hasCollapsibleNodes = Object.keys(collapsedStates).length > 0; + const h1Count = headlines.filter((h) => h.tag === "h1").length; + const firstH1Index = headlines.findIndex((h) => h.tag === "h1"); return (
+ {h1Count === 0 && ( + + )} + {h1Count > 1 && ( + + )} {hasCollapsibleNodes && (

@@ -197,6 +249,8 @@ export function HeadlineTree({ headlines, keyword }: HeadlineTreeProps) { keyword={keyword} collapsedStates={collapsedStates} onToggle={handleToggle} + parentLevel={0} + isDuplicateH1={headline.tag === "h1" && index !== firstH1Index} /> ))}