Spaces:
Configuration error
Configuration error
| 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, | |
| }, | |
| })), | |
| } | |
| } | |