feat(seo): upgrade site metadata, schema, and crawl files for OpenFrame rebrand

- update title/description to Open Source Video Review Platform
- add centralized SEO config with canonical URL + meta image (/meta.webp)
- add Open Graph and Twitter card metadata
- add JSON-LD Organization/WebSite structured data
- add robots.txt, sitemap.xml, and web app manifest routes
- version icon metadata to help search engines refresh branding
This commit is contained in:
Yusuf İpek
2026-02-22 16:53:01 +03:00
parent b0e6f9514a
commit 323093029e
6 changed files with 202 additions and 3 deletions
+92 -3
View File
@@ -1,7 +1,8 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist_Mono, JetBrains_Mono } from "next/font/google"; import { Geist_Mono, JetBrains_Mono } from "next/font/google";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import { ThemeProvider } from "@/components/theme-provider";
import { seoConfig } from "@/lib/seo";
import "./globals.css"; import "./globals.css";
const jetbrainsMono = JetBrains_Mono({ const jetbrainsMono = JetBrains_Mono({
@@ -19,10 +20,94 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "OpenFrame - Video Feedback Platform", metadataBase: new URL(seoConfig.url),
description: "Collect timestamped video feedback with text and voice comments", title: {
default: `${seoConfig.name} | ${seoConfig.title}`,
template: `%s | ${seoConfig.name}`,
},
description: seoConfig.description,
applicationName: seoConfig.name,
keywords: [...seoConfig.keywords],
authors: [{ name: seoConfig.name, url: seoConfig.url }],
creator: seoConfig.name,
publisher: seoConfig.name,
category: "technology",
referrer: "origin-when-cross-origin",
alternates: {
canonical: "/",
},
icons: {
icon: [{ url: seoConfig.logo, type: "image/svg+xml" }],
shortcut: [seoConfig.logo],
apple: [{ url: seoConfig.logo }],
},
manifest: "/manifest.webmanifest",
openGraph: {
type: "website",
locale: "en_US",
siteName: seoConfig.name,
url: seoConfig.url,
title: `${seoConfig.name} | ${seoConfig.title}`,
description: seoConfig.description,
images: [
{
url: seoConfig.ogImage,
width: 1888,
height: 1048,
alt: `${seoConfig.name} meta image`,
},
],
},
twitter: {
card: "summary_large_image",
title: `${seoConfig.name} | ${seoConfig.title}`,
description: seoConfig.description,
images: [seoConfig.ogImage],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
"max-video-preview": -1,
},
},
formatDetection: {
email: false,
address: false,
telephone: false,
},
}; };
const structuredData = [
{
"@context": "https://schema.org",
"@type": "Organization",
name: seoConfig.name,
url: seoConfig.url,
logo: `${seoConfig.url}${seoConfig.logoPath}`,
sameAs: [seoConfig.githubUrl],
},
{
"@context": "https://schema.org",
"@type": "WebSite",
name: seoConfig.name,
url: seoConfig.url,
description: seoConfig.description,
publisher: {
"@type": "Organization",
name: seoConfig.name,
logo: {
"@type": "ImageObject",
url: `${seoConfig.url}${seoConfig.logoPath}`,
},
},
},
];
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
@@ -31,6 +116,10 @@ export default function RootLayout({
return ( return (
<html lang="en" className={`${jetbrainsMono.variable} ${geistMono.variable}`} suppressHydrationWarning> <html lang="en" className={`${jetbrainsMono.variable} ${geistMono.variable}`} suppressHydrationWarning>
<body className="antialiased min-h-screen bg-background font-sans"> <body className="antialiased min-h-screen bg-background font-sans">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="dark" defaultTheme="dark"
+22
View File
@@ -0,0 +1,22 @@
import type { MetadataRoute } from 'next';
import { seoConfig } from '@/lib/seo';
export default function manifest(): MetadataRoute.Manifest {
return {
name: `${seoConfig.name} - ${seoConfig.title}`,
short_name: seoConfig.name,
description: seoConfig.description,
start_url: '/',
display: 'standalone',
background_color: '#0a0a0a',
theme_color: '#0a0a0a',
icons: [
{
src: seoConfig.logo,
sizes: 'any',
type: 'image/svg+xml',
purpose: 'any',
},
],
};
}
+16
View File
@@ -0,0 +1,16 @@
import type { MetadataRoute } from 'next';
import { seoConfig } from '@/lib/seo';
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin', '/dashboard', '/projects', '/settings', '/workspaces'],
},
],
sitemap: `${seoConfig.url}/sitemap.xml`,
host: seoConfig.url,
};
}
+27
View File
@@ -0,0 +1,27 @@
import type { MetadataRoute } from 'next';
import { seoConfig } from '@/lib/seo';
export default function sitemap(): MetadataRoute.Sitemap {
const lastModified = new Date();
return [
{
url: seoConfig.url,
lastModified,
changeFrequency: 'daily',
priority: 1,
},
{
url: `${seoConfig.url}/login`,
lastModified,
changeFrequency: 'monthly',
priority: 0.6,
},
{
url: `${seoConfig.url}/register`,
lastModified,
changeFrequency: 'monthly',
priority: 0.7,
},
];
}
+45
View File
@@ -0,0 +1,45 @@
const FALLBACK_SITE_URL = 'https://openframe.net';
function normalizeSiteUrl(rawUrl: string | undefined): string {
if (!rawUrl) {
return FALLBACK_SITE_URL;
}
const trimmed = rawUrl.trim();
if (!trimmed) {
return FALLBACK_SITE_URL;
}
const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
try {
return new URL(withProtocol).origin;
} catch {
return FALLBACK_SITE_URL;
}
}
export function getSiteUrl(): string {
return normalizeSiteUrl(process.env.NEXT_PUBLIC_APP_URL ?? process.env.NEXTAUTH_URL);
}
export const seoConfig = {
name: 'OpenFrame',
title: 'Open Source Video Review Platform',
description:
'OpenFrame is an open source video review platform for collecting timestamped feedback with text and voice comments.',
keywords: [
'open source video review platform',
'video review tool',
'timestamped video feedback',
'video collaboration',
'video annotation',
'creative review workflow',
],
url: getSiteUrl(),
ogImage: '/meta.webp',
logoPath: '/icon.svg',
logo: '/icon.svg?v=2',
githubUrl: 'https://github.com/yusufipk/OpenFrame',
} as const;
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB