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 }) => (
+
+);
+
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}
/>
))}