Improve SEO metadata
This commit is contained in:
@@ -16,6 +16,7 @@ type LocaleMessages = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const siteUrl = "https://kairas.io";
|
||||||
const alternates = Object.fromEntries([
|
const alternates = Object.fromEntries([
|
||||||
...routing.locales.map((locale) => [locale, `/${locale}`]),
|
...routing.locales.map((locale) => [locale, `/${locale}`]),
|
||||||
["x-default", `/${routing.defaultLocale}`],
|
["x-default", `/${routing.defaultLocale}`],
|
||||||
@@ -41,13 +42,38 @@ export async function generateMetadata({
|
|||||||
const messages = await getLocaleMessages(locale);
|
const messages = await getLocaleMessages(locale);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metadataBase: new URL("https://kairas.io"),
|
metadataBase: new URL(siteUrl),
|
||||||
title: messages.Meta.title,
|
title: messages.Meta.title,
|
||||||
description: messages.Meta.description,
|
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: {
|
alternates: {
|
||||||
canonical: `/${locale}`,
|
canonical: `/${locale}`,
|
||||||
languages: alternates,
|
languages: alternates,
|
||||||
},
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
"max-image-preview": "large",
|
||||||
|
"max-snippet": -1,
|
||||||
|
"max-video-preview": -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: messages.Meta.title,
|
title: messages.Meta.title,
|
||||||
description: messages.Meta.description,
|
description: messages.Meta.description,
|
||||||
@@ -55,6 +81,23 @@ export async function generateMetadata({
|
|||||||
siteName: "Kairas",
|
siteName: "Kairas",
|
||||||
locale: locale.replace("-", "_"),
|
locale: locale.replace("-", "_"),
|
||||||
type: "website",
|
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 work = t.raw("work.items") as WorkItem[];
|
||||||
const processSteps = t.raw("process.steps") as ProcessStep[];
|
const processSteps = t.raw("process.steps") as ProcessStep[];
|
||||||
const notes = t.raw("notes.items") as NoteItem[];
|
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(() => {
|
useEffect(() => {
|
||||||
const root = rootRef.current;
|
const root = rootRef.current;
|
||||||
@@ -211,6 +261,10 @@ export default function Home() {
|
|||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
className="relative min-h-screen bg-[var(--beige)] text-[var(--navy)]"
|
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="grain fixed inset-0 z-0" />
|
||||||
<div className="pointer-events-none fixed inset-0 z-50 grid grid-cols-3">
|
<div className="pointer-events-none fixed inset-0 z-50 grid grid-cols-3">
|
||||||
<div className="intro-mask bg-[var(--navy)]" />
|
<div className="intro-mask bg-[var(--navy)]" />
|
||||||
@@ -218,7 +272,10 @@ export default function Home() {
|
|||||||
<div className="intro-mask bg-[var(--navy)]" />
|
<div className="intro-mask bg-[var(--navy)]" />
|
||||||
</div>
|
</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">
|
<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]">
|
<Link href="/" className="nav-item text-lg font-semibold tracking-[0.18em]">
|
||||||
Kairas
|
Kairas
|
||||||
</Link>
|
</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