[dyad] Added link analysis feature - wrote 4 file(s)
This commit is contained in:
@@ -21,6 +21,13 @@ export interface ImageAltData {
|
||||
size: number | null;
|
||||
}
|
||||
|
||||
export interface LinkData {
|
||||
href: string;
|
||||
text: string;
|
||||
type: "internal" | "external" | "anchor" | "other";
|
||||
rel: string;
|
||||
}
|
||||
|
||||
export async function extractMetaData(url: string, keyword?: string) {
|
||||
if (!url) {
|
||||
return { error: "URL is required." };
|
||||
@@ -175,7 +182,6 @@ export async function extractMetaData(url: string, keyword?: string) {
|
||||
|
||||
const imageSizePromises = imageSrcs.map(async (img) => {
|
||||
try {
|
||||
// Use a HEAD request for efficiency
|
||||
const res = await fetch(img.src, { method: "HEAD" });
|
||||
if (res.ok) {
|
||||
const contentLength = res.headers.get("content-length");
|
||||
@@ -192,6 +198,33 @@ export async function extractMetaData(url: string, keyword?: string) {
|
||||
|
||||
const imageAltData: ImageAltData[] = await Promise.all(imageSizePromises);
|
||||
|
||||
const links: LinkData[] = [];
|
||||
const pageUrl = new URL(formattedUrl);
|
||||
|
||||
$("a").each((i, el) => {
|
||||
const href = $(el).attr("href");
|
||||
if (!href) return;
|
||||
|
||||
const text = $(el).text().trim();
|
||||
const rel = $(el).attr("rel") || "";
|
||||
let type: LinkData["type"] = "external";
|
||||
let absoluteUrl = href;
|
||||
|
||||
try {
|
||||
const linkUrl = new URL(href, formattedUrl);
|
||||
absoluteUrl = linkUrl.href;
|
||||
if (linkUrl.hostname === pageUrl.hostname) {
|
||||
type = "internal";
|
||||
}
|
||||
} catch (e) {
|
||||
if (href.startsWith("#")) type = "anchor";
|
||||
else if (href.startsWith("mailto:") || href.startsWith("tel:"))
|
||||
type = "other";
|
||||
}
|
||||
|
||||
links.push({ href: absoluteUrl, text, type, rel });
|
||||
});
|
||||
|
||||
return {
|
||||
data: {
|
||||
title,
|
||||
@@ -207,6 +240,7 @@ export async function extractMetaData(url: string, keyword?: string) {
|
||||
keyword: trimmedKeyword || null,
|
||||
keywordCount,
|
||||
images: imageAltData.length > 0 ? imageAltData : null,
|
||||
links: links.length > 0 ? links : null,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
134
src/components/links-display.tsx
Normal file
134
src/components/links-display.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExternalLink, Link as LinkIcon } from "lucide-react";
|
||||
import type { LinkData } from "@/app/actions";
|
||||
|
||||
interface LinksDisplayProps {
|
||||
links: LinkData[];
|
||||
}
|
||||
|
||||
const StatCard = ({ title, value }: { title: string; value: number }) => (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export function LinksDisplay({ links }: LinksDisplayProps) {
|
||||
const [filter, setFilter] = useState<"all" | "internal" | "external">("all");
|
||||
|
||||
const stats = useMemo(() => {
|
||||
return {
|
||||
total: links.length,
|
||||
internal: links.filter((l) => l.type === "internal").length,
|
||||
external: links.filter((l) => l.type === "external").length,
|
||||
nofollow: links.filter((l) => l.rel.includes("nofollow")).length,
|
||||
};
|
||||
}, [links]);
|
||||
|
||||
const filteredLinks = useMemo(() => {
|
||||
if (filter === "all") return links;
|
||||
return links.filter((l) => l.type === filter);
|
||||
}, [links, filter]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard title="Total Links" value={stats.total} />
|
||||
<StatCard title="Internal Links" value={stats.internal} />
|
||||
<StatCard title="External Links" value={stats.external} />
|
||||
<StatCard title="Nofollow Links" value={stats.nofollow} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={filter === "all" ? "default" : "outline"}
|
||||
onClick={() => setFilter("all")}
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === "internal" ? "default" : "outline"}
|
||||
onClick={() => setFilter("internal")}
|
||||
>
|
||||
Internal
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === "external" ? "default" : "outline"}
|
||||
onClick={() => setFilter("external")}
|
||||
>
|
||||
External
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>URL</TableHead>
|
||||
<TableHead>Anchor Text</TableHead>
|
||||
<TableHead className="text-center">Type</TableHead>
|
||||
<TableHead className="text-center">Attributes</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredLinks.map((link, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="max-w-xs truncate">
|
||||
<a
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 hover:underline"
|
||||
>
|
||||
{link.type === "external" ? (
|
||||
<ExternalLink className="h-4 w-4 flex-shrink-0" />
|
||||
) : (
|
||||
<LinkIcon className="h-4 w-4 flex-shrink-0" />
|
||||
)}
|
||||
<span className="truncate">{link.href}</span>
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell>{link.text || "No anchor text"}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant="secondary" className="capitalize">
|
||||
{link.type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{link.rel ? (
|
||||
<div className="flex justify-center gap-1">
|
||||
{link.rel.split(" ").map((r) => (
|
||||
<Badge key={r} variant="outline">
|
||||
{r}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { SchemaDisplay } from "./schema-display";
|
||||
import { MetaFormInputs } from "./meta-form-inputs";
|
||||
import { AnalysisTab } from "./analysis-tab";
|
||||
import { SocialTab } from "./social-tab";
|
||||
import { LinksDisplay } from "./links-display";
|
||||
import type { MetaData } from "@/lib/types";
|
||||
|
||||
export function MetaForm() {
|
||||
@@ -108,6 +109,10 @@ export function MetaForm() {
|
||||
}
|
||||
}
|
||||
|
||||
// Links Tab
|
||||
const linksColor: IndicatorColor =
|
||||
metaData.links && metaData.links.length > 0 ? "green" : "gray";
|
||||
|
||||
// FAQ Tab
|
||||
let faqColor: IndicatorColor = "gray";
|
||||
if (metaData.faq && metaData.faq.length > 0) {
|
||||
@@ -125,6 +130,7 @@ export function MetaForm() {
|
||||
headlines: headlinesColor,
|
||||
images: imagesColor,
|
||||
social: socialColor,
|
||||
links: linksColor,
|
||||
faq: faqColor,
|
||||
schema: schemaColor,
|
||||
};
|
||||
@@ -200,6 +206,12 @@ export function MetaForm() {
|
||||
Images
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{metaData.links && metaData.links.length > 0 && (
|
||||
<TabsTrigger value="links">
|
||||
{tabColors && <TabIndicator color={tabColors.links} />}
|
||||
Links
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{metaData.openGraph && metaData.twitter && (
|
||||
<TabsTrigger value="social">
|
||||
{tabColors && <TabIndicator color={tabColors.social} />}
|
||||
@@ -263,6 +275,16 @@ export function MetaForm() {
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{metaData.links && metaData.links.length > 0 && (
|
||||
<TabsContent value="links">
|
||||
<Card className="w-full shadow-lg rounded-lg">
|
||||
<CardContent className="p-6">
|
||||
<LinksDisplay links={metaData.links} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{metaData.openGraph && metaData.twitter && (
|
||||
<TabsContent value="social">
|
||||
<SocialTab
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HeadlineNode, ImageAltData } from "@/app/actions";
|
||||
import type { HeadlineNode, ImageAltData, LinkData } from "@/app/actions";
|
||||
|
||||
export interface OpenGraphData {
|
||||
title: string;
|
||||
@@ -32,4 +32,5 @@ export interface MetaData {
|
||||
robots?: string | null;
|
||||
openGraph?: OpenGraphData;
|
||||
twitter?: TwitterData;
|
||||
links?: LinkData[] | null;
|
||||
}
|
||||
Reference in New Issue
Block a user