Improve SEO metadata

This commit is contained in:
Codex
2026-05-05 00:33:35 +01:00
parent f6ace49f2f
commit fb43cd6804
6 changed files with 285 additions and 2 deletions

View File

@@ -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",
},
};
}

View 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,
);
}

View File

@@ -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
View 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
View 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
View 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}`],
]),
},
}));
}