export interface SEOConfig { title: string description: string keywords?: string[] image?: string url?: string type?: 'website' | 'article' | 'profile' publishedTime?: string modifiedTime?: string author?: string section?: string } const BASE_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://open-prompt.netlify.app' // Use dynamic OG route as default (no static og-image.png needed) const DEFAULT_IMAGE = `${BASE_URL}/api/og?title=OpenPrompt&description=The+GitHub+for+AI+Prompts` /** * Generate a dynamic OG image URL via /api/og */ export function generateOGImageUrl(params: { title: string description?: string type?: 'default' | 'prompt' | 'tool' | 'creator' | 'collection' category?: string runs?: number stars?: number remixes?: number creator?: string badge?: string }): string { const url = new URL(`${BASE_URL}/api/og`) url.searchParams.set('title', params.title.slice(0, 80)) if (params.description) url.searchParams.set('description', params.description.slice(0, 140)) if (params.type) url.searchParams.set('type', params.type) if (params.category) url.searchParams.set('category', params.category) if (params.runs !== undefined) url.searchParams.set('runs', formatCount(params.runs)) if (params.stars !== undefined) url.searchParams.set('stars', formatCount(params.stars)) if (params.remixes !== undefined) url.searchParams.set('remixes', formatCount(params.remixes)) if (params.creator) url.searchParams.set('creator', params.creator) if (params.badge) url.searchParams.set('badge', params.badge) return url.toString() } function formatCount(n: number): string { if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M' if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\.0$/, '') + 'K' return n.toString() } export function generateSEO(config: SEOConfig) { const { title, description, keywords = [], image = DEFAULT_IMAGE, url = BASE_URL, type = 'website', publishedTime, modifiedTime, author, section, } = config const fullTitle = title.includes('OpenPrompt') ? title : `${title} | OpenPrompt` return { title: fullTitle, description, keywords: [ 'AI prompts', 'prompt engineering', 'ChatGPT prompts', 'Claude prompts', 'AI tools', 'prompt marketplace', ...keywords, ], authors: [{ name: author || 'OpenPrompt', url: 'https://github.com/Anky9972' }], creator: 'OpenPrompt', publisher: 'OpenPrompt', metadataBase: new URL(BASE_URL), alternates: { canonical: url, }, openGraph: { title: fullTitle, description, url, siteName: 'OpenPrompt', images: [ { url: image, width: 1200, height: 630, alt: title, }, ], locale: 'en_US', type, ...(publishedTime && { publishedTime }), ...(modifiedTime && { modifiedTime }), ...(section && { section }), }, twitter: { card: 'summary_large_image', title: fullTitle, description, images: [image], creator: '@anky_vivek', site: '@openprompt', }, robots: { index: true, follow: true, googleBot: { index: true, follow: true, 'max-video-preview': -1, 'max-image-preview': 'large', 'max-snippet': -1, }, }, verification: { google: 'google60f35921954050fd', }, } } // JSON-LD Structured Data generators export function generateWebsiteSchema() { return { '@context': 'https://schema.org', '@type': 'WebSite', name: 'OpenPrompt', description: 'The GitHub for AI Prompts - Transform AI prompts into shareable micro-apps', url: BASE_URL, potentialAction: { '@type': 'SearchAction', target: { '@type': 'EntryPoint', urlTemplate: `${BASE_URL}/explore?q={search_term_string}`, }, 'query-input': 'required name=search_term_string', }, sameAs: [ 'https://github.com/Anky9972/open-prompt', 'https://twitter.com/anky_vivek', ], } } export function generateOrganizationSchema() { return { '@context': 'https://schema.org', '@type': 'Organization', name: 'OpenPrompt', url: BASE_URL, logo: `${BASE_URL}/logos/logo.svg`, description: 'The GitHub for AI Prompts - Transform AI prompts into shareable micro-apps', sameAs: [ 'https://github.com/Anky9972/open-prompt', 'https://twitter.com/anky_vivek', ], } } export function generateSoftwareApplicationSchema() { return { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'OpenPrompt', applicationCategory: 'DeveloperApplication', operatingSystem: 'Web', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD', }, description: 'AI Prompts Marketplace with 177+ tools, multi-model support, and browser extension', featureList: [ 'AI Prompts Marketplace', '177+ AI Tools', 'Multi-Model Support (GPT-4, Claude, Gemini)', 'Browser Extension', 'Workflow Automation', 'Model Comparison (Thunderdome)', ], } } export function generatePromptSchema(prompt: { title: string description?: string slug: string creator?: { name?: string } createdAt: Date updatedAt: Date starsCount: number totalRuns?: number remixesCount?: number category?: string }) { const hasEnoughRatings = prompt.starsCount >= 5 const normalizedRating = hasEnoughRatings ? Math.min(5, Math.max(1, 1 + (prompt.starsCount / (prompt.starsCount + 10)) * 4)) : undefined return { '@context': 'https://schema.org', '@type': 'CreativeWork', name: prompt.title, description: prompt.description || `AI prompt: ${prompt.title}`, url: `${BASE_URL}/p/${prompt.slug}`, author: { '@type': 'Person', name: prompt.creator?.name || 'Anonymous', }, dateCreated: prompt.createdAt.toISOString(), dateModified: prompt.updatedAt.toISOString(), ...(hasEnoughRatings && normalizedRating && { aggregateRating: { '@type': 'AggregateRating', ratingValue: normalizedRating.toFixed(1), ratingCount: prompt.starsCount, reviewCount: prompt.starsCount, bestRating: 5, worstRating: 1, }, }), genre: prompt.category || 'AI Prompt', isAccessibleForFree: true, ...(prompt.totalRuns && { interactionStatistic: { '@type': 'InteractionCounter', interactionType: 'https://schema.org/UseAction', userInteractionCount: prompt.totalRuns, }, }), } } export function generateToolSchema(tool: { name: string description: string slug: string category: string }) { return { '@context': 'https://schema.org', '@type': 'WebApplication', name: tool.name, description: tool.description, url: `${BASE_URL}/tools/${tool.slug}`, applicationCategory: 'Productivity', operatingSystem: 'Web', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD', }, isAccessibleForFree: true, } } export function generateBreadcrumbSchema(items: { name: string; url: string }[]) { return { '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: items.map((item, index) => ({ '@type': 'ListItem', position: index + 1, name: item.name, item: item.url, })), } } export function generateFAQSchema(faqs: { question: string; answer: string }[]) { return { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faqs.map((faq) => ({ '@type': 'Question', name: faq.question, acceptedAnswer: { '@type': 'Answer', text: faq.answer, }, })), } }