diff --git a/app/[locale]/animation-sources/[slug]/page.tsx b/app/[locale]/animation-sources/[slug]/page.tsx index 447bb10..1dadfed 100644 --- a/app/[locale]/animation-sources/[slug]/page.tsx +++ b/app/[locale]/animation-sources/[slug]/page.tsx @@ -40,23 +40,36 @@ export async function generateMetadata({ } const url = `${SITE_URL}${getLocalizedPath(locale, source.slug)}`; + const socialImageUrl = new URL(source.coverImage, SITE_URL).toString(); + const turkishUrl = `${SITE_URL}${getLocalizedPath("tr", source.slug)}`; + const englishUrl = `${SITE_URL}${getLocalizedPath("en", source.slug)}`; return { title: source.title, description: source.excerpt, - alternates: { canonical: url }, + alternates: { + canonical: url, + languages: { + "tr-TR": turkishUrl, + "en-US": englishUrl, + }, + }, openGraph: { title: source.title, description: source.excerpt, url, + siteName: "Poyraz Avsever", type: "article", + locale: locale === "en" ? "en_US" : "tr_TR", + alternateLocale: locale === "en" ? ["tr_TR"] : ["en_US"], publishedTime: source.date, authors: [source.author], images: [ { - url: source.coverImage, - width: 720, - height: 720, + url: socialImageUrl, + type: "image/gif", + width: 480, + height: 480, alt: source.title, }, ], @@ -65,7 +78,13 @@ export async function generateMetadata({ card: "summary_large_image", title: source.title, description: source.excerpt, - images: [source.coverImage], + creator: "@poyrazavsever", + images: [ + { + url: socialImageUrl, + alt: source.title, + }, + ], }, }; } diff --git a/app/[locale]/animation-sources/page.tsx b/app/[locale]/animation-sources/page.tsx index 87739db..3f6d67e 100644 --- a/app/[locale]/animation-sources/page.tsx +++ b/app/[locale]/animation-sources/page.tsx @@ -3,6 +3,9 @@ import { getTranslations } from "next-intl/server"; import { AnimationSourcesContent } from "@/components/animation-sources-content"; import { listAnimationSources } from "@/data/animation-sources"; +const SITE_URL = + process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com"; + type AnimationSourcesPageProps = { params: Promise<{ locale: string }>; }; @@ -11,13 +14,49 @@ export async function generateMetadata({ params, }: AnimationSourcesPageProps): Promise { const { locale } = await params; - const t = await getTranslations({ locale, namespace: "AnimationSources" }); + const [t, sources] = await Promise.all([ + getTranslations({ locale, namespace: "AnimationSources" }), + listAnimationSources(locale), + ]); + const localizedPath = locale === "en" ? "/en/animation-sources" : "/animation-sources"; + const url = `${SITE_URL}${localizedPath}`; + const socialImagePath = sources[0]?.coverImage ?? "/logo/logo.png"; + const socialImageUrl = new URL(socialImagePath, SITE_URL).toString(); + const isGif = socialImagePath.toLowerCase().endsWith(".gif"); return { title: t("title"), description: t("description"), alternates: { - canonical: "/animation-sources", + canonical: url, + languages: { + "tr-TR": `${SITE_URL}/animation-sources`, + "en-US": `${SITE_URL}/en/animation-sources`, + }, + }, + openGraph: { + title: t("title"), + description: t("description"), + url, + siteName: "Poyraz Avsever", + type: "website", + locale: locale === "en" ? "en_US" : "tr_TR", + images: [ + { + url: socialImageUrl, + type: isGif ? "image/gif" : "image/png", + width: isGif ? 480 : 1200, + height: isGif ? 480 : 1200, + alt: t("title"), + }, + ], + }, + twitter: { + card: "summary_large_image", + title: t("title"), + description: t("description"), + creator: "@poyrazavsever", + images: [{ url: socialImageUrl, alt: t("title") }], }, }; } diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index cb47c26..58bccc0 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -8,6 +8,7 @@ import "../globals.css"; import { AppShell } from "@/components/app-shell"; import { GoogleAnalytics } from "@/components/google-analytics"; import { PoyrazBottomRightFollower } from "@/components/poyraz-bottom-right-follower"; +import { listAnimationSources } from "@/data/animation-sources"; export async function generateMetadata({ params, @@ -108,14 +109,26 @@ export default async function LocaleLayout({ } // Provide messages for NextIntlClientProvider - const messages = await getMessages(); + const [messages, animationSources] = await Promise.all([ + getMessages(), + listAnimationSources(locale), + ]); + const animationSourceSearchItems = animationSources.map((source) => ({ + slug: source.slug, + title: source.title, + excerpt: source.excerpt, + platform: source.platform, + tools: source.tools, + })); return ( - {children} + + {children} + diff --git a/components/animation-sources-content.tsx b/components/animation-sources-content.tsx index 900df8b..442eb22 100644 --- a/components/animation-sources-content.tsx +++ b/components/animation-sources-content.tsx @@ -1,5 +1,5 @@ -import Image from "next/image"; -import { Badge, Card, Typography } from "poyraz-ui/atoms"; +import { Card, Typography } from "poyraz-ui/atoms"; +import { NewsCard } from "poyraz-ui/molecules"; import { Link } from "@/i18n/routing"; import type { AnimationSource } from "@/data/animation-sources"; @@ -34,38 +34,21 @@ export function AnimationSourcesContent({ {sources.length > 0 ? ( -
+
{sources.map((source) => ( - -
- -
-
- - {source.platform} - - - {source.title} - -
-
+ ))}
diff --git a/components/app-shell.tsx b/components/app-shell.tsx index 52bfe92..0fa9e47 100644 --- a/components/app-shell.tsx +++ b/components/app-shell.tsx @@ -9,6 +9,7 @@ import { ANNOUNCEMENT_ITEMS, ENABLE_NEKO_FOLLOWER } from "@/data/site-settings"; import { useLocale } from "next-intl"; import { getLocalizedValue } from "@/lib/locale"; import dynamic from "next/dynamic"; +import type { AnimationSourceSearchItem } from "@/lib/command-palette-links"; const AtaturkWidgetModal = dynamic( () => import("@/components/ataturk-widget-modal").then((mod) => mod.AtaturkWidgetModal), @@ -17,6 +18,7 @@ const AtaturkWidgetModal = dynamic( type AppShellProps = { children: React.ReactNode; + animationSources: AnimationSourceSearchItem[]; }; export type ThemeMode = "light" | "dark"; @@ -30,7 +32,7 @@ function getInitialTheme(): ThemeMode { return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } -export function AppShell({ children }: AppShellProps) { +export function AppShell({ children, animationSources }: AppShellProps) { const pathname = usePathname(); const locale = useLocale(); const announcement = ANNOUNCEMENT_ITEMS[0]; @@ -57,7 +59,11 @@ export function AppShell({ children }: AppShellProps) { {ENABLE_NEKO_FOLLOWER ? : null}
- + {announcement ? ( void; + animationSources: AnimationSourceSearchItem[]; }; -export function SearchCommand({ open, onOpenChange }: SearchCommandProps) { +export function SearchCommand({ + open, + onOpenChange, + animationSources, +}: SearchCommandProps) { const router = useRouter(); const locale = useLocale(); const tLinks = useTranslations("Links"); @@ -36,8 +42,8 @@ export function SearchCommand({ open, onOpenChange }: SearchCommandProps) { const [query, setQuery] = useState(""); const groups = useMemo(() => { - return getCommandPaletteGroups(locale, tLinks, tNav); - }, [locale, tLinks, tNav]); + return getCommandPaletteGroups(locale, tLinks, tNav, animationSources); + }, [animationSources, locale, tLinks, tNav]); const handleOpenChange = useCallback((nextOpen: boolean) => { if (!nextOpen) { diff --git a/components/site-navbar.tsx b/components/site-navbar.tsx index 0867a37..6a0565f 100644 --- a/components/site-navbar.tsx +++ b/components/site-navbar.tsx @@ -43,6 +43,7 @@ import { TOP_ICON_LINKS, } from "@/lib/links"; import type { ThemeMode } from "@/components/app-shell"; +import type { AnimationSourceSearchItem } from "@/lib/command-palette-links"; const SearchCommand = dynamic( () => import("@/components/search-command").then((mod) => mod.SearchCommand), @@ -98,9 +99,15 @@ function ThemeToggle({ theme, onThemeChange }: ThemeToggleProps) { ); } -type SiteNavbarProps = ThemeToggleProps; +type SiteNavbarProps = ThemeToggleProps & { + animationSources: AnimationSourceSearchItem[]; +}; -export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) { +export function SiteNavbar({ + theme, + onThemeChange, + animationSources, +}: SiteNavbarProps) { const pathname = usePathname(); const [searchOpen, setSearchOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); @@ -545,7 +552,11 @@ export function SiteNavbar({ theme, onThemeChange }: SiteNavbarProps) {
- +
); diff --git a/lib/command-palette-links.ts b/lib/command-palette-links.ts index 38764b9..c9bacd5 100644 --- a/lib/command-palette-links.ts +++ b/lib/command-palette-links.ts @@ -28,10 +28,19 @@ export type CommandPaletteGroup = { items: CommandPaletteItem[]; }; +export type AnimationSourceSearchItem = { + slug: string; + title: string; + excerpt: string; + platform: string; + tools: string[]; +}; + export function getCommandPaletteGroups( locale: string, tLinks: (key: string) => string, - tNav: { (key: string): string; has: (key: string) => boolean } + tNav: { (key: string): string; has: (key: string) => boolean }, + animationSources: AnimationSourceSearchItem[] = [], ): CommandPaletteGroup[] { const navigationItems: CommandPaletteItem[] = NAV_LINKS.map((item) => { const label = tNav.has(item.id) ? tNav(item.id) : item.label; @@ -77,6 +86,24 @@ export function getCommandPaletteGroups( }), ); + const animationSourceItems: CommandPaletteItem[] = animationSources.map( + (source) => ({ + id: `animation-source-${source.slug}`, + label: source.title, + href: `/animation-sources/${source.slug}`, + icon: "mdi:motion-play-outline", + keywords: [ + source.excerpt, + source.platform, + ...source.tools, + "animation", + "prompt", + locale === "tr" ? "animasyon" : "motion", + locale === "tr" ? "kaynak" : "source", + ], + }), + ); + const blogItems: CommandPaletteItem[] = [ { id: "blog-index", @@ -297,6 +324,17 @@ export function getCommandPaletteGroups( items: contentItems, }, ...dropdownGroups, + ...(animationSourceItems.length > 0 + ? [ + { + id: "animation-sources-data", + heading: tNav.has("animationResources") + ? tNav("animationResources") + : "Animation Sources", + items: animationSourceItems, + }, + ] + : []), { id: "social", heading: "Social",