[dyad] Add headline hierarchy validation - wrote 1 file(s)

This commit is contained in:
[dyad]
2026-01-21 08:52:08 +01:00
parent 589a5f7827
commit 1f204a523b

View File

@@ -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 }) => (
<div className="flex items-center gap-3 p-4 text-sm text-destructive-foreground bg-destructive/80 border-b">
<AlertTriangle className="h-5 w-5 flex-shrink-0" />
<p className="font-medium">{message}</p>
</div>
);
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<string, boolean>;
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 = (
<div
className={cn(
"flex items-center gap-3 py-2.5 pr-4 border-t",
isCollapsible && "cursor-pointer hover:bg-muted/50",
hasError && "bg-destructive/10"
)}
style={{ paddingLeft: `${16 + level * 24}px` }}
onClick={() => isCollapsible && onToggle(path)}
>
<Badge
variant={hasError ? "destructive" : "secondary"}
className="uppercase w-12 flex-shrink-0 justify-center font-mono"
>
{node.tag}
</Badge>
<p className="font-medium flex-grow text-foreground">
<KeywordHighlighter text={node.text} keyword={keyword} />
</p>
<p className="text-sm text-muted-foreground flex-shrink-0 w-10 text-right">
{node.length}
</p>
{isCollapsible && (
<ChevronDown
className={cn(
"h-4 w-4 text-muted-foreground transition-transform",
!isCollapsed && "rotate-180"
)}
/>
)}
</div>
);
return (
<>
<div
className={cn(
"flex items-center gap-3 py-2.5 pr-4 border-t",
isCollapsible && "cursor-pointer hover:bg-muted/50"
)}
style={{ paddingLeft: `${16 + level * 24}px` }}
onClick={() => isCollapsible && onToggle(path)}
>
<Badge
variant="secondary"
className="uppercase w-12 flex-shrink-0 justify-center font-mono"
>
{node.tag}
</Badge>
<p className="font-medium flex-grow text-foreground">
<KeywordHighlighter text={node.text} keyword={keyword} />
</p>
<p className="text-sm text-muted-foreground flex-shrink-0 w-10 text-right">
{node.length}
</p>
{isCollapsible && (
<ChevronDown
className={cn(
"h-4 w-4 text-muted-foreground transition-transform",
!isCollapsed && "rotate-180"
)}
/>
)}
</div>
{errorTooltip ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent>
<p>{errorTooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
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 (
<div className="w-full rounded-lg overflow-hidden">
{h1Count === 0 && (
<HierarchyWarning message="No H1 tag found. Every page should have exactly one H1." />
)}
{h1Count > 1 && (
<HierarchyWarning message="Multiple H1 tags found. Only one H1 is recommended per page." />
)}
{hasCollapsibleNodes && (
<div className="flex items-center justify-between p-4 border-b bg-muted/50">
<h4 className="font-semibold text-sm text-muted-foreground">
@@ -197,6 +249,8 @@ export function HeadlineTree({ headlines, keyword }: HeadlineTreeProps) {
keyword={keyword}
collapsedStates={collapsedStates}
onToggle={handleToggle}
parentLevel={0}
isDuplicateH1={headline.tag === "h1" && index !== firstH1Index}
/>
))}
</div>