feat: improve SEO with JSON-LD metadata, robots.txt, and sitemap configuration
This commit is contained in:
+53
-2
@@ -14,8 +14,59 @@ const anuphan = Anuphan({
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const siteUrl = "https://erika.sudloh.com";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Erika",
|
||||
title: {
|
||||
default: "Erika! - แหล่งรวมลิ้งค์ของช่อง Erika",
|
||||
template: "%s | Erika!",
|
||||
},
|
||||
description:
|
||||
"แหล่งรวมลิ้งค์ทั้งหมดของช่อง Erika!",
|
||||
keywords: ["Erika", "erika!", "erika jjs", "สุดหล่อ", "Roblox", "Discord", "TikTok", "YouTube", "Linktree", "ลิ้งค์"],
|
||||
metadataBase: new URL(siteUrl),
|
||||
alternates: {
|
||||
canonical: "/",
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "th_TH",
|
||||
url: siteUrl,
|
||||
siteName: "Erika!",
|
||||
title: "Erika! - แหล่งรวมลิ้งค์ของช่อง Erika",
|
||||
description:
|
||||
"แหล่งรวมลิ้งค์ทั้งหมดของช่อง Erika!",
|
||||
images: [
|
||||
{
|
||||
url: "/avatar.webp",
|
||||
width: 512,
|
||||
height: 512,
|
||||
alt: "Erika! avatar",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title: "Erika! - แหล่งรวมลิ้งค์ของช่อง Erika",
|
||||
description:
|
||||
"แหล่งรวมลิ้งค์ทั้งหมดของช่อง Erika! ติดตามได้ทุกช่องทาง Roblox, Discord, TikTok, YouTube และอื่นๆ",
|
||||
images: ["/avatar.webp"],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
icons: {
|
||||
icon: "/favicon.webp",
|
||||
apple: "/avatar.webp",
|
||||
},
|
||||
};
|
||||
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
@@ -28,7 +79,7 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
lang="th"
|
||||
className={cn("h-full", "dark", "antialiased", anuphan.variable, geistSans.variable, "font-sans")}
|
||||
>
|
||||
<body className={`${anuphan.className} ${geistSans.className}`}>
|
||||
|
||||
+60
-39
@@ -6,16 +6,35 @@ import Link from "next/link";
|
||||
import ShaderBackground from "@/components/ShaderBackground";
|
||||
|
||||
export default function Home() {
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfilePage",
|
||||
name: config.profile.name,
|
||||
description: config.profile.desc,
|
||||
url: "https://erika.sudloh.com",
|
||||
mainEntity: {
|
||||
"@type": "Person",
|
||||
name: config.profile.name,
|
||||
image: "https://erika.sudloh.com/avatar.webp",
|
||||
sameAs: config.links.map((l) => l.link),
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center p-4">
|
||||
<main className="flex min-h-screen flex-col items-center justify-center p-4">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<ShaderBackground />
|
||||
<Card className="w-full max-w-md overflow-hidden relative">
|
||||
<Card className="w-full max-w-md overflow-hidden relative" role="article">
|
||||
<CardHeader className="flex flex-col items-center gap-4 text-center">
|
||||
<div className="relative w-24 h-24 overflow-hidden rounded-full">
|
||||
<Image
|
||||
src={config.profile.avatar}
|
||||
alt={config.profile.name}
|
||||
alt={`${config.profile.name} avatar`}
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
@@ -29,46 +48,48 @@ export default function Home() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
{config.links.map((item, index) => {
|
||||
const isStringIcon = typeof item.icon === "string";
|
||||
const IconComponent = !isStringIcon ? item.icon : null;
|
||||
<CardContent>
|
||||
<nav aria-label="ลิ้งค์โซเชียลมีเดีย" className="flex flex-col gap-3">
|
||||
{config.links.map((item, index) => {
|
||||
const isStringIcon = typeof item.icon === "string";
|
||||
const IconComponent = !isStringIcon ? item.icon : null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="w-full h-auto p-4 group"
|
||||
asChild
|
||||
>
|
||||
<Link href={item.link} target="_blank" rel="noopener noreferrer" className="w-full">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="shrink-0 w-8 h-8 flex items-center justify-center">
|
||||
{isStringIcon ? (
|
||||
<Image
|
||||
src={item.icon as string}
|
||||
alt={`${item.name} icon`}
|
||||
width={24}
|
||||
height={24}
|
||||
className="object-contain"
|
||||
/>
|
||||
) : (
|
||||
IconComponent && <IconComponent className="w-6 h-6 text-zinc-700 dark:text-zinc-300" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-start text-left">
|
||||
<span className="font-semibold text-zinc-900 dark:text-zinc-100">{item.name}</span>
|
||||
{item.desc && <span className="text-xs text-zinc-500 dark:text-zinc-400">{item.desc}</span>}
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="w-full h-auto p-4 group"
|
||||
asChild
|
||||
>
|
||||
<Link href={item.link} target="_blank" rel="noopener noreferrer" className="w-full">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="shrink-0 w-8 h-8 flex items-center justify-center">
|
||||
{isStringIcon ? (
|
||||
<Image
|
||||
src={item.icon as string}
|
||||
alt={`${item.name} icon`}
|
||||
width={24}
|
||||
height={24}
|
||||
className="object-contain"
|
||||
/>
|
||||
) : (
|
||||
IconComponent && <IconComponent className="w-6 h-6 text-zinc-700 dark:text-zinc-300" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-start text-left">
|
||||
<span className="font-semibold text-zinc-900 dark:text-zinc-100">{item.name}</span>
|
||||
{item.desc && <span className="text-xs text-zinc-500 dark:text-zinc-400">{item.desc}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
disallow: ["/api/", "/form/admin/"],
|
||||
},
|
||||
],
|
||||
sitemap: "https://erika.sudloh.com/sitemap.xml",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = "https://erika.sudloh.com";
|
||||
|
||||
return [
|
||||
{
|
||||
url: baseUrl,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/form`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/discord`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.5,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user