[dyad] Added image alt tag analysis - wrote 3 file(s)
This commit is contained in:
@@ -15,6 +15,11 @@ export interface HeadlineNode {
|
|||||||
children: HeadlineNode[];
|
children: HeadlineNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageAltData {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function extractMetaData(url: string, keyword?: string) {
|
export async function extractMetaData(url: string, keyword?: string) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return { error: "URL is required." };
|
return { error: "URL is required." };
|
||||||
@@ -122,6 +127,24 @@ export async function extractMetaData(url: string, keyword?: string) {
|
|||||||
keywordCount = matches ? matches.length : 0;
|
keywordCount = matches ? matches.length : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageAltData: ImageAltData[] = [];
|
||||||
|
$("img").each((i, el) => {
|
||||||
|
const src = $(el).attr("src");
|
||||||
|
const alt = $(el).attr("alt") || "";
|
||||||
|
|
||||||
|
if (src) {
|
||||||
|
try {
|
||||||
|
const absoluteSrc = new URL(src, formattedUrl).href;
|
||||||
|
imageAltData.push({
|
||||||
|
src: absoluteSrc,
|
||||||
|
alt: alt.trim(),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore invalid URLs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
@@ -131,6 +154,7 @@ export async function extractMetaData(url: string, keyword?: string) {
|
|||||||
headlines: headlines.length > 0 ? headlines : null,
|
headlines: headlines.length > 0 ? headlines : null,
|
||||||
keyword: trimmedKeyword || null,
|
keyword: trimmedKeyword || null,
|
||||||
keywordCount,
|
keywordCount,
|
||||||
|
images: imageAltData.length > 0 ? imageAltData : null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
75
src/components/image-alt-display.tsx
Normal file
75
src/components/image-alt-display.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { ImageAltData } from "@/app/actions";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { ImageOff } from "lucide-react";
|
||||||
|
|
||||||
|
interface ImageAltDisplayProps {
|
||||||
|
images: ImageAltData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImageAltDisplay({ images }: ImageAltDisplayProps) {
|
||||||
|
const [showMissingOnly, setShowMissingOnly] = useState(false);
|
||||||
|
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const filteredImages = showMissingOnly
|
||||||
|
? images.filter((img) => !img.alt)
|
||||||
|
: images;
|
||||||
|
|
||||||
|
const handleImageError = (src: string) => {
|
||||||
|
setImageErrors((prev) => ({ ...prev, [src]: true }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="missing-alt"
|
||||||
|
checked={showMissingOnly}
|
||||||
|
onCheckedChange={(checked) => setShowMissingOnly(!!checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="missing-alt">
|
||||||
|
Show only images with missing alt text
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredImages.map((image, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-start gap-4 p-4 border rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="w-24 h-24 flex-shrink-0 bg-muted rounded-md flex items-center justify-center overflow-hidden">
|
||||||
|
{imageErrors[image.src] ? (
|
||||||
|
<ImageOff className="h-8 w-8 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={image.src}
|
||||||
|
alt={image.alt || "Image preview"}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onError={() => handleImageError(image.src)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<p className="text-sm text-muted-foreground break-all">
|
||||||
|
{image.src}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
{image.alt ? (
|
||||||
|
<p className="text-sm text-foreground bg-muted/50 p-2 rounded-md">
|
||||||
|
<span className="font-semibold">Alt:</span> {image.alt}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<Badge variant="destructive">Missing alt text</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
ImageOff,
|
ImageOff,
|
||||||
Search,
|
Search,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { extractMetaData, type HeadlineNode } from "@/app/actions";
|
import { extractMetaData, type HeadlineNode, type ImageAltData } from "@/app/actions";
|
||||||
import { LengthIndicator } from "./length-indicator";
|
import { LengthIndicator } from "./length-indicator";
|
||||||
import { CopyButton } from "./copy-button";
|
import { CopyButton } from "./copy-button";
|
||||||
import { SerpPreview } from "./serp-preview";
|
import { SerpPreview } from "./serp-preview";
|
||||||
@@ -22,6 +22,7 @@ import { ResultsSkeleton } from "./results-skeleton";
|
|||||||
import { FaqDisplay } from "./faq-display";
|
import { FaqDisplay } from "./faq-display";
|
||||||
import { HeadlineTree } from "./headline-tree";
|
import { HeadlineTree } from "./headline-tree";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { ImageAltDisplay } from "./image-alt-display";
|
||||||
|
|
||||||
interface MetaData {
|
interface MetaData {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -31,6 +32,7 @@ interface MetaData {
|
|||||||
headlines?: HeadlineNode[] | null;
|
headlines?: HeadlineNode[] | null;
|
||||||
keyword?: string | null;
|
keyword?: string | null;
|
||||||
keywordCount?: number | null;
|
keywordCount?: number | null;
|
||||||
|
images?: ImageAltData[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MetaForm() {
|
export function MetaForm() {
|
||||||
@@ -151,6 +153,9 @@ export function MetaForm() {
|
|||||||
{metaData.headlines && metaData.headlines.length > 0 && (
|
{metaData.headlines && metaData.headlines.length > 0 && (
|
||||||
<TabsTrigger value="headlines">Headlines</TabsTrigger>
|
<TabsTrigger value="headlines">Headlines</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
|
{metaData.images && metaData.images.length > 0 && (
|
||||||
|
<TabsTrigger value="images">Images</TabsTrigger>
|
||||||
|
)}
|
||||||
{metaData.faq && metaData.faq.length > 0 && (
|
{metaData.faq && metaData.faq.length > 0 && (
|
||||||
<TabsTrigger value="faq">FAQ</TabsTrigger>
|
<TabsTrigger value="faq">FAQ</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
@@ -335,6 +340,16 @@ export function MetaForm() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{metaData.images && metaData.images.length > 0 && (
|
||||||
|
<TabsContent value="images">
|
||||||
|
<Card className="w-full shadow-lg rounded-lg">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<ImageAltDisplay images={metaData.images} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
{metaData.faq && metaData.faq.length > 0 && (
|
{metaData.faq && metaData.faq.length > 0 && (
|
||||||
<TabsContent value="faq">
|
<TabsContent value="faq">
|
||||||
<Card className="w-full shadow-lg rounded-lg">
|
<Card className="w-full shadow-lg rounded-lg">
|
||||||
|
|||||||
Reference in New Issue
Block a user