Improve SEO metadata
This commit is contained in:
@@ -16,6 +16,7 @@ type LocaleMessages = {
|
||||
};
|
||||
};
|
||||
|
||||
const siteUrl = "https://kairas.io";
|
||||
const alternates = Object.fromEntries([
|
||||
...routing.locales.map((locale) => [locale, `/${locale}`]),
|
||||
["x-default", `/${routing.defaultLocale}`],
|
||||
@@ -41,13 +42,38 @@ export async function generateMetadata({
|
||||
const messages = await getLocaleMessages(locale);
|
||||
|
||||
return {
|
||||
metadataBase: new URL("https://kairas.io"),
|
||||
metadataBase: new URL(siteUrl),
|
||||
title: messages.Meta.title,
|
||||
description: messages.Meta.description,
|
||||
applicationName: "Kairas",
|
||||
authors: [{name: "Kairas", url: siteUrl}],
|
||||
creator: "Kairas",
|
||||
publisher: "Kairas",
|
||||
category: "Web design",
|
||||
keywords: [
|
||||
"Kairas",
|
||||
"web design",
|
||||
"web design studio",
|
||||
"brand websites",
|
||||
"design systems",
|
||||
"frontend development",
|
||||
"digital products",
|
||||
],
|
||||
alternates: {
|
||||
canonical: `/${locale}`,
|
||||
languages: alternates,
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
"max-video-preview": -1,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: messages.Meta.title,
|
||||
description: messages.Meta.description,
|
||||
@@ -55,6 +81,23 @@ export async function generateMetadata({
|
||||
siteName: "Kairas",
|
||||
locale: locale.replace("-", "_"),
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: `/${locale}/opengraph-image`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: messages.Meta.title,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: messages.Meta.title,
|
||||
description: messages.Meta.description,
|
||||
images: [`/${locale}/opengraph-image`],
|
||||
},
|
||||
icons: {
|
||||
icon: "/icon.svg",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
120
src/app/[locale]/opengraph-image.tsx
Normal file
120
src/app/[locale]/opengraph-image.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import {ImageResponse} from "next/og";
|
||||
import {hasLocale} from "next-intl";
|
||||
import {localeDirections, routing, type Locale} from "@/i18n/routing";
|
||||
import arMessages from "../../../messages/ar.json";
|
||||
import bnMessages from "../../../messages/bn.json";
|
||||
import enMessages from "../../../messages/en.json";
|
||||
import esMessages from "../../../messages/es.json";
|
||||
import frMessages from "../../../messages/fr.json";
|
||||
import hiMessages from "../../../messages/hi.json";
|
||||
import ptMessages from "../../../messages/pt.json";
|
||||
import ruMessages from "../../../messages/ru.json";
|
||||
import urMessages from "../../../messages/ur.json";
|
||||
import zhMessages from "../../../messages/zh.json";
|
||||
|
||||
export const runtime = "edge";
|
||||
export const alt = "Kairas web design studio";
|
||||
export const size = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
};
|
||||
export const contentType = "image/png";
|
||||
|
||||
type OpenGraphImageProps = {
|
||||
params: Promise<{locale: string}>;
|
||||
};
|
||||
|
||||
type LocaleMessages = {
|
||||
Meta: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
Home: {
|
||||
hero: {
|
||||
kicker: string;
|
||||
words: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const localeMessages: Record<Locale, LocaleMessages> = {
|
||||
en: enMessages,
|
||||
zh: zhMessages,
|
||||
hi: hiMessages,
|
||||
es: esMessages,
|
||||
fr: frMessages,
|
||||
ar: arMessages,
|
||||
bn: bnMessages,
|
||||
pt: ptMessages,
|
||||
ru: ruMessages,
|
||||
ur: urMessages,
|
||||
};
|
||||
|
||||
export default async function OpenGraphImage({params}: OpenGraphImageProps) {
|
||||
const {locale: requestedLocale} = await params;
|
||||
const locale = hasLocale(routing.locales, requestedLocale)
|
||||
? requestedLocale
|
||||
: routing.defaultLocale;
|
||||
const messages = localeMessages[locale];
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
padding: 64,
|
||||
color: "#081b33",
|
||||
background: "#eee4d4",
|
||||
fontFamily: "Georgia, serif",
|
||||
direction: localeDirections[locale],
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
fontSize: 28,
|
||||
letterSpacing: "0.16em",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
<span>Kairas</span>
|
||||
<span>{messages.Home.hero.kicker}</span>
|
||||
</div>
|
||||
<div style={{display: "flex", flexDirection: "column", gap: 12}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 122,
|
||||
lineHeight: 0.9,
|
||||
fontWeight: 700,
|
||||
maxWidth: 980,
|
||||
}}
|
||||
>
|
||||
{messages.Home.hero.words.join(" ")}
|
||||
</div>
|
||||
<div style={{fontSize: 30, lineHeight: 1.35, maxWidth: 820}}>
|
||||
{messages.Meta.description}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
borderTop: "1px solid rgba(8, 27, 51, 0.22)",
|
||||
paddingTop: 26,
|
||||
fontSize: 26,
|
||||
}}
|
||||
>
|
||||
<span>Strategy / Design / Development</span>
|
||||
<span>kairas.io</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
size,
|
||||
);
|
||||
}
|
||||
@@ -33,6 +33,56 @@ export default function Home() {
|
||||
const work = t.raw("work.items") as WorkItem[];
|
||||
const processSteps = t.raw("process.steps") as ProcessStep[];
|
||||
const notes = t.raw("notes.items") as NoteItem[];
|
||||
const siteUrl = "https://kairas.io";
|
||||
const pageUrl = `${siteUrl}/${locale}`;
|
||||
const structuredData = [
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfessionalService",
|
||||
"@id": `${siteUrl}/#organization`,
|
||||
name: "Kairas",
|
||||
url: siteUrl,
|
||||
email: "hello@kairas.io",
|
||||
image: `${pageUrl}/opengraph-image`,
|
||||
description: t("hero.copy"),
|
||||
areaServed: "Worldwide",
|
||||
serviceType: services.map((service) => service.title),
|
||||
knowsAbout: [
|
||||
"Web design",
|
||||
"Brand websites",
|
||||
"Design systems",
|
||||
"Frontend development",
|
||||
"Digital product design",
|
||||
],
|
||||
sameAs: [siteUrl],
|
||||
},
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"@id": `${siteUrl}/#website`,
|
||||
name: "Kairas",
|
||||
url: siteUrl,
|
||||
inLanguage: locale,
|
||||
publisher: {
|
||||
"@id": `${siteUrl}/#organization`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"@id": `${pageUrl}#webpage`,
|
||||
url: pageUrl,
|
||||
name: "Kairas",
|
||||
description: t("hero.copy"),
|
||||
inLanguage: locale,
|
||||
isPartOf: {
|
||||
"@id": `${siteUrl}/#website`,
|
||||
},
|
||||
about: {
|
||||
"@id": `${siteUrl}/#organization`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const root = rootRef.current;
|
||||
@@ -211,6 +261,10 @@ export default function Home() {
|
||||
ref={rootRef}
|
||||
className="relative min-h-screen bg-[var(--beige)] text-[var(--navy)]"
|
||||
>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{__html: JSON.stringify(structuredData)}}
|
||||
/>
|
||||
<div className="grain fixed inset-0 z-0" />
|
||||
<div className="pointer-events-none fixed inset-0 z-50 grid grid-cols-3">
|
||||
<div className="intro-mask bg-[var(--navy)]" />
|
||||
@@ -218,7 +272,10 @@ export default function Home() {
|
||||
<div className="intro-mask bg-[var(--navy)]" />
|
||||
</div>
|
||||
<header className="fixed left-0 right-0 top-0 z-20 border-b border-[var(--line)] bg-[rgba(238,228,212,0.86)] backdrop-blur">
|
||||
<nav className="mx-auto flex max-w-[1500px] items-center justify-between gap-4 px-5 py-4 text-[13px] uppercase leading-none tracking-[0.12em] sm:px-8 lg:px-12">
|
||||
<nav
|
||||
aria-label="Primary"
|
||||
className="mx-auto flex max-w-[1500px] items-center justify-between gap-4 px-5 py-4 text-[13px] uppercase leading-none tracking-[0.12em] sm:px-8 lg:px-12"
|
||||
>
|
||||
<Link href="/" className="nav-item text-lg font-semibold tracking-[0.18em]">
|
||||
Kairas
|
||||
</Link>
|
||||
|
||||
22
src/app/manifest.ts
Normal file
22
src/app/manifest.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type {MetadataRoute} from "next";
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: "Kairas",
|
||||
short_name: "Kairas",
|
||||
description:
|
||||
"Kairas is a web design firm crafting refined websites, brand systems, and digital products.",
|
||||
start_url: "/en",
|
||||
display: "standalone",
|
||||
background_color: "#eee4d4",
|
||||
theme_color: "#081b33",
|
||||
icons: [
|
||||
{
|
||||
src: "/icon.svg",
|
||||
sizes: "any",
|
||||
type: "image/svg+xml",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
16
src/app/robots.ts
Normal file
16
src/app/robots.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type {MetadataRoute} from "next";
|
||||
|
||||
const siteUrl = "https://kairas.io";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
disallow: ["/_next/"],
|
||||
},
|
||||
sitemap: `${siteUrl}/sitemap.xml`,
|
||||
host: siteUrl,
|
||||
};
|
||||
}
|
||||
|
||||
25
src/app/sitemap.ts
Normal file
25
src/app/sitemap.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type {MetadataRoute} from "next";
|
||||
import {routing} from "@/i18n/routing";
|
||||
|
||||
const siteUrl = "https://kairas.io";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const now = new Date();
|
||||
|
||||
return routing.locales.map((locale) => ({
|
||||
url: `${siteUrl}/${locale}`,
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: locale === routing.defaultLocale ? 1 : 0.8,
|
||||
alternates: {
|
||||
languages: Object.fromEntries([
|
||||
...routing.locales.map((alternateLocale) => [
|
||||
alternateLocale,
|
||||
`${siteUrl}/${alternateLocale}`,
|
||||
]),
|
||||
["x-default", `${siteUrl}/${routing.defaultLocale}`],
|
||||
]),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user