[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 { Badge } from "@/components/ui/badge";
import type { HeadlineNode } from "@/app/actions"; import type { HeadlineNode } from "@/app/actions";
import { KeywordHighlighter } from "./keyword-highlighter"; 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 { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -36,6 +41,13 @@ const getCollapsiblePaths = (
return paths; 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 = ({ const HeadlineNodeDisplay = ({
node, node,
level, level,
@@ -43,30 +55,44 @@ const HeadlineNodeDisplay = ({
keyword, keyword,
collapsedStates, collapsedStates,
onToggle, onToggle,
parentLevel,
isDuplicateH1,
}: { }: {
node: HeadlineNode; node: HeadlineNode;
level: number; level: number;
path: string; path:string;
keyword?: string | null; keyword?: string | null;
collapsedStates: Record<string, boolean>; collapsedStates: Record<string, boolean>;
onToggle: (path: string) => void; onToggle: (path: string) => void;
parentLevel: number;
isDuplicateH1: boolean;
}) => { }) => {
const isCollapsible = const isCollapsible =
node.level === 2 && node.children && node.children.length > 0; node.level === 2 && node.children && node.children.length > 0;
const isCollapsed = collapsedStates[path]; const isCollapsed = collapsedStates[path];
return ( 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 <div
className={cn( className={cn(
"flex items-center gap-3 py-2.5 pr-4 border-t", "flex items-center gap-3 py-2.5 pr-4 border-t",
isCollapsible && "cursor-pointer hover:bg-muted/50" isCollapsible && "cursor-pointer hover:bg-muted/50",
hasError && "bg-destructive/10"
)} )}
style={{ paddingLeft: `${16 + level * 24}px` }} style={{ paddingLeft: `${16 + level * 24}px` }}
onClick={() => isCollapsible && onToggle(path)} onClick={() => isCollapsible && onToggle(path)}
> >
<Badge <Badge
variant="secondary" variant={hasError ? "destructive" : "secondary"}
className="uppercase w-12 flex-shrink-0 justify-center font-mono" className="uppercase w-12 flex-shrink-0 justify-center font-mono"
> >
{node.tag} {node.tag}
@@ -86,6 +112,22 @@ const HeadlineNodeDisplay = ({
/> />
)} )}
</div> </div>
);
return (
<>
{errorTooltip ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent>
<p>{errorTooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
content
)}
{node.children && {node.children &&
(!isCollapsible || !isCollapsed) && (!isCollapsible || !isCollapsed) &&
node.children.map((child, index) => ( node.children.map((child, index) => (
@@ -97,6 +139,8 @@ const HeadlineNodeDisplay = ({
keyword={keyword} keyword={keyword}
collapsedStates={collapsedStates} collapsedStates={collapsedStates}
onToggle={onToggle} 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 hasCollapsibleNodes = Object.keys(collapsedStates).length > 0;
const h1Count = headlines.filter((h) => h.tag === "h1").length;
const firstH1Index = headlines.findIndex((h) => h.tag === "h1");
return ( return (
<div className="w-full rounded-lg overflow-hidden"> <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 && ( {hasCollapsibleNodes && (
<div className="flex items-center justify-between p-4 border-b bg-muted/50"> <div className="flex items-center justify-between p-4 border-b bg-muted/50">
<h4 className="font-semibold text-sm text-muted-foreground"> <h4 className="font-semibold text-sm text-muted-foreground">
@@ -197,6 +249,8 @@ export function HeadlineTree({ headlines, keyword }: HeadlineTreeProps) {
keyword={keyword} keyword={keyword}
collapsedStates={collapsedStates} collapsedStates={collapsedStates}
onToggle={handleToggle} onToggle={handleToggle}
parentLevel={0}
isDuplicateH1={headline.tag === "h1" && index !== firstH1Index}
/> />
))} ))}
</div> </div>