[dyad] Added live SERP preview - wrote 2 file(s)
This commit is contained in:
@@ -9,6 +9,7 @@ import { Globe, Edit, Check, Loader2, X, ImageOff } from "lucide-react";
|
|||||||
import { extractMetaData } from "@/app/actions";
|
import { extractMetaData } 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";
|
||||||
|
|
||||||
interface MetaData {
|
interface MetaData {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -21,7 +22,7 @@ export function MetaForm() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [metaData, setMetaData] = useState<MetaData | null>(null);
|
const [metaData, setMetaData] = useState<MetaData | null>(null);
|
||||||
|
|
||||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||||
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
||||||
|
|
||||||
@@ -120,6 +121,17 @@ export function MetaForm() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-card-foreground mb-2">
|
||||||
|
SERP Preview
|
||||||
|
</h3>
|
||||||
|
<SerpPreview
|
||||||
|
title={editableTitle}
|
||||||
|
description={editableDescription}
|
||||||
|
url={url}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{metaData.image && (
|
{metaData.image && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-card-foreground mb-2">
|
<h3 className="font-semibold text-card-foreground mb-2">
|
||||||
@@ -169,7 +181,9 @@ export function MetaForm() {
|
|||||||
) : (
|
) : (
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
<span className="sr-only">{isEditingTitle ? "Done editing" : "Edit"}</span>
|
<span className="sr-only">
|
||||||
|
{isEditingTitle ? "Done editing" : "Edit"}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,7 +222,9 @@ export function MetaForm() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setIsEditingDescription(!isEditingDescription)}
|
onClick={() =>
|
||||||
|
setIsEditingDescription(!isEditingDescription)
|
||||||
|
}
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
>
|
>
|
||||||
{isEditingDescription ? (
|
{isEditingDescription ? (
|
||||||
@@ -216,12 +232,15 @@ export function MetaForm() {
|
|||||||
) : (
|
) : (
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
<span className="sr-only">{isEditingDescription ? "Done editing" : "Edit"}</span>
|
<span className="sr-only">
|
||||||
|
{isEditingDescription ? "Done editing" : "Edit"}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mt-1 mb-2">
|
<p className="text-sm text-muted-foreground mt-1 mb-2">
|
||||||
A brief summary of the page's content, ideally between 120 and 158 characters.
|
A brief summary of the page's content, ideally between 120 and
|
||||||
|
158 characters.
|
||||||
</p>
|
</p>
|
||||||
{isEditingDescription ? (
|
{isEditingDescription ? (
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
52
src/components/serp-preview.tsx
Normal file
52
src/components/serp-preview.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Globe } from "lucide-react";
|
||||||
|
|
||||||
|
interface SerpPreviewProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SerpPreview({ title, description, url }: SerpPreviewProps) {
|
||||||
|
const formatUrl = (fullUrl: string) => {
|
||||||
|
try {
|
||||||
|
let formattedUrl = fullUrl;
|
||||||
|
if (!/^https?:\/\//i.test(fullUrl)) {
|
||||||
|
formattedUrl = `https://${fullUrl}`;
|
||||||
|
}
|
||||||
|
const urlObject = new URL(formattedUrl);
|
||||||
|
const path = urlObject.pathname === "/" ? "" : urlObject.pathname;
|
||||||
|
// remove trailing slash
|
||||||
|
const displayPath = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
|
return `${urlObject.hostname}${displayPath}`;
|
||||||
|
} catch (error) {
|
||||||
|
return fullUrl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayUrl = formatUrl(url);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border rounded-lg bg-muted/50 w-full font-sans">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-7 h-7 bg-background rounded-full flex items-center justify-center border">
|
||||||
|
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-foreground font-medium -mb-0.5">
|
||||||
|
{displayUrl.split("/")[0]}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{displayUrl}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl text-blue-600 dark:text-blue-400 mt-2 truncate font-medium">
|
||||||
|
{title || "Meta Title Preview"}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{description ||
|
||||||
|
"This is where the meta description will be displayed. It provides a brief summary of the page's content for search engine users."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user