mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
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:
+92
-3
@@ -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"
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
@@ -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;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
Reference in New Issue
Block a user