fix(seo): correct canonicals and navigation semantics

This commit is contained in:
poyrazavsever
2026-08-31 20:08:11 +03:00
parent 9d5f347ffb
commit ddeab06b45
7 changed files with 57 additions and 36 deletions
+3 -2
View File
@@ -10,6 +10,7 @@ import { GoogleAnalytics } from "@/components/google-analytics";
import { PoyrazBottomRightFollower } from "@/components/poyraz-bottom-right-follower"; import { PoyrazBottomRightFollower } from "@/components/poyraz-bottom-right-follower";
import { listAnimationSources } from "@/data/animation-sources"; import { listAnimationSources } from "@/data/animation-sources";
import { getHomeBlogNews, getLatestAgendaArticle } from "@/data/blog"; import { getHomeBlogNews, getLatestAgendaArticle } from "@/data/blog";
import { SITE_URL } from "@/lib/seo";
export async function generateMetadata({ export async function generateMetadata({
params, params,
@@ -24,7 +25,7 @@ export async function generateMetadata({
const description = t("description"); const description = t("description");
return { return {
metadataBase: new URL("https://poyrazavsever.com"), metadataBase: new URL(SITE_URL),
title: { title: {
default: title, default: title,
template: titleTemplate, template: titleTemplate,
@@ -34,7 +35,7 @@ export async function generateMetadata({
icons: { icons: {
icon: [ icon: [
{ url: "/favicon.ico", sizes: "any" }, { url: "/favicon.ico", sizes: "any" },
{ url: "/logo/logo.webp", type: "image/webp" }, { url: "/logo/logo-96.webp", type: "image/webp", sizes: "96x96" },
], ],
shortcut: "/favicon.ico", shortcut: "/favicon.ico",
apple: "/logo/apple-touch-icon.png", apple: "/logo/apple-touch-icon.png",
+1 -2
View File
@@ -1,6 +1,5 @@
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next";
import { SITE_URL } from "@/lib/seo";
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
export default function robots(): MetadataRoute.Robots { export default function robots(): MetadataRoute.Robots {
return { return {
+1 -1
View File
@@ -1,6 +1,6 @@
import { getAllBlogArticles } from "@/data/blog"; import { getAllBlogArticles } from "@/data/blog";
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com"; import { SITE_URL } from "@/lib/seo";
const FEED_PATH = "/rss.xml"; const FEED_PATH = "/rss.xml";
const FEED_TITLE = "Poyraz Avsever Blog"; const FEED_TITLE = "Poyraz Avsever Blog";
const FEED_DESCRIPTION = const FEED_DESCRIPTION =
+1 -1
View File
@@ -1,4 +1,4 @@
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com"; import { SITE_URL } from "@/lib/seo";
function serializeJsonLd(data: object) { function serializeJsonLd(data: object) {
return JSON.stringify(data).replace(/</g, "\\u003c"); return JSON.stringify(data).replace(/</g, "\\u003c");
+10 -5
View File
@@ -235,7 +235,7 @@ function PromoRail({
> >
<div className="sticky top-4 py-4"> <div className="sticky top-4 py-4">
<div <div
role="tablist" role="group"
aria-label={t("slideNavigationLabel")} aria-label={t("slideNavigationLabel")}
onKeyDown={handleNavigationKeyDown} onKeyDown={handleNavigationKeyDown}
className="mb-3 flex h-5 items-center justify-center gap-2" className="mb-3 flex h-5 items-center justify-center gap-2"
@@ -247,9 +247,7 @@ function PromoRail({
<button <button
key={`${railId}-dot-${index}`} key={`${railId}-dot-${index}`}
type="button" type="button"
role="tab" aria-pressed={selected}
aria-selected={selected}
aria-controls={`${railId}-panel-${index}`}
aria-label={t("slideCta", { slide: index + 1 })} aria-label={t("slideCta", { slide: index + 1 })}
onClick={() => setActiveSlide(index)} onClick={() => setActiveSlide(index)}
className="group flex size-5 items-center justify-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" className="group flex size-5 items-center justify-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
@@ -266,12 +264,19 @@ function PromoRail({
})} })}
</div> </div>
<span className="sr-only" aria-live="polite">
{t("slideStatus", {
current: activeSlide + 1,
total: slides.length,
})}
</span>
<div className="overflow-hidden"> <div className="overflow-hidden">
<AnimatePresence mode="wait" initial={false}> <AnimatePresence mode="wait" initial={false}>
<motion.div <motion.div
key={`${railId}-slide-${activeSlide}`} key={`${railId}-slide-${activeSlide}`}
id={`${railId}-panel-${activeSlide}`} id={`${railId}-panel-${activeSlide}`}
role="tabpanel" role="group"
aria-label={t("slideStatus", { aria-label={t("slideStatus", {
current: activeSlide + 1, current: activeSlide + 1,
total: slides.length, total: slides.length,
+26 -22
View File
@@ -25,9 +25,6 @@ import {
SheetContent, SheetContent,
SheetTitle, SheetTitle,
SheetTrigger, SheetTrigger,
Tabs,
TabsList,
TabsTrigger,
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
@@ -74,6 +71,18 @@ function getDesktopDropdownTriggerClass(isActive: boolean) {
].join(" "); ].join(" ");
} }
function getDesktopNavLinkClass(isActive: boolean) {
return [
"relative inline-flex h-9 shrink-0 items-center justify-center whitespace-nowrap rounded-sm px-2.5 text-xs font-medium outline-none",
"transition-[color,background-color] duration-[var(--poyraz-motion-duration-fast)] ease-[var(--poyraz-motion-ease-out)]",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"after:absolute after:inset-x-2.5 after:bottom-0 after:h-0.5 after:rounded-full after:transition-colors",
isActive
? "text-foreground after:bg-primary"
: "text-muted-foreground after:bg-transparent hover:text-foreground",
].join(" ");
}
type ThemeToggleProps = { type ThemeToggleProps = {
theme: ThemeMode; theme: ThemeMode;
onThemeChange: (theme: ThemeMode) => void; onThemeChange: (theme: ThemeMode) => void;
@@ -125,7 +134,6 @@ export function SiteNavbar({
return pathname === href || pathname.startsWith(`${href}/`); return pathname === href || pathname.startsWith(`${href}/`);
}; };
const activeTab = NAV_LINKS.find((item) => isActiveLink(item.href))?.id;
const activeMobileMenuGroup = NAV_DROPDOWN_GROUPS.find( const activeMobileMenuGroup = NAV_DROPDOWN_GROUPS.find(
(group) => group.id === mobileMenuGroupId, (group) => group.id === mobileMenuGroupId,
); );
@@ -204,7 +212,7 @@ export function SiteNavbar({
className="inline-flex shrink-0 items-center" className="inline-flex shrink-0 items-center"
> >
<Logo <Logo
src="/logo/logo.webp" src="/logo/logo-96.webp"
alt="Poyraz Avsever" alt="Poyraz Avsever"
width={40} width={40}
height={40} height={40}
@@ -216,13 +224,10 @@ export function SiteNavbar({
</Link> </Link>
<div className="hidden min-w-0 flex-1 items-center justify-end gap-2 min-[840px]:flex"> <div className="hidden min-w-0 flex-1 items-center justify-end gap-2 min-[840px]:flex">
<Tabs value={activeTab ?? ""} className="w-auto shrink-0"> <nav
<TabsList aria-label={locale === "tr" ? "Ana navigasyon" : "Main navigation"}
variant="line" className="flex h-9 w-auto shrink-0 items-center gap-1"
radius="sm" >
className="h-9 gap-1 bg-transparent p-0"
aria-label="Ana navigasyon"
>
{NAV_LINKS.map((item, index) => ( {NAV_LINKS.map((item, index) => (
<Fragment key={item.id}> <Fragment key={item.id}>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
@@ -233,15 +238,15 @@ export function SiteNavbar({
decorative decorative
/> />
)} )}
<TabsTrigger <Link
value={item.id} href={item.href}
asChild aria-current={isActiveLink(item.href) ? "page" : undefined}
size="sm" className={getDesktopNavLinkClass(
radius="sm" isActiveLink(item.href),
className="cursor-pointer px-2.5" )}
> >
<Link href={item.href}>{t(item.id)}</Link> {t(item.id)}
</TabsTrigger> </Link>
</div> </div>
{NAV_DROPDOWN_GROUPS.filter( {NAV_DROPDOWN_GROUPS.filter(
@@ -316,8 +321,7 @@ export function SiteNavbar({
))} ))}
</Fragment> </Fragment>
))} ))}
</TabsList> </nav>
</Tabs>
<Separator <Separator
orientation="vertical" orientation="vertical"
+15 -3
View File
@@ -1,9 +1,21 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
export const SITE_URL = ( function resolveSiteUrl() {
process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com" const url = new URL(
).replace(/\/$/, ""); process.env.NEXT_PUBLIC_SITE_URL || "https://www.poyrazavsever.com",
);
// The public site redirects the apex domain to `www`. Canonicals must point
// directly to the final URL instead of a redirecting host.
if (url.hostname === "poyrazavsever.com") {
url.hostname = "www.poyrazavsever.com";
}
return url.origin;
}
export const SITE_URL = resolveSiteUrl();
export type SiteLocale = "tr" | "en"; export type SiteLocale = "tr" | "en";