feat: Ultra SEO optimization - sitemap, robots, structured data, 800+ TLD pages
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
This commit is contained in:
9
frontend/src/app/(public)/layout.tsx
Normal file
9
frontend/src/app/(public)/layout.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Public pages layout - inherits from root layout
|
||||||
|
export default function PublicLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
68
frontend/src/app/acquire/layout.tsx
Normal file
68
frontend/src/app/acquire/layout.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
import Script from 'next/script'
|
||||||
|
import { SEO_CONFIG, SITE_URL, generateWebPageSchema, generateBreadcrumbSchema } from '@/lib/seo'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: SEO_CONFIG.acquire.title,
|
||||||
|
description: SEO_CONFIG.acquire.description,
|
||||||
|
keywords: SEO_CONFIG.acquire.keywords,
|
||||||
|
alternates: {
|
||||||
|
canonical: `${SITE_URL}/acquire`,
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: SEO_CONFIG.acquire.title,
|
||||||
|
description: SEO_CONFIG.acquire.description,
|
||||||
|
url: `${SITE_URL}/acquire`,
|
||||||
|
siteName: 'Pounce',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${SITE_URL}/og-acquire.png`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Domain Auctions & Marketplace - Pounce',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: 'en_US',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: SEO_CONFIG.acquire.title,
|
||||||
|
description: SEO_CONFIG.acquire.description,
|
||||||
|
images: [`${SITE_URL}/og-acquire.png`],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AcquireLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const webPageSchema = generateWebPageSchema({
|
||||||
|
title: SEO_CONFIG.acquire.title,
|
||||||
|
description: SEO_CONFIG.acquire.description,
|
||||||
|
url: `${SITE_URL}/acquire`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const breadcrumbSchema = generateBreadcrumbSchema([
|
||||||
|
{ name: 'Home', url: SITE_URL },
|
||||||
|
{ name: 'Domain Marketplace', url: `${SITE_URL}/acquire` },
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Script
|
||||||
|
id="acquire-webpage-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(webPageSchema) }}
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="acquire-breadcrumb-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
68
frontend/src/app/discover/layout.tsx
Normal file
68
frontend/src/app/discover/layout.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
import Script from 'next/script'
|
||||||
|
import { SEO_CONFIG, SITE_URL, DEFAULT_OG_IMAGE, generateWebPageSchema, generateBreadcrumbSchema } from '@/lib/seo'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: SEO_CONFIG.discover.title,
|
||||||
|
description: SEO_CONFIG.discover.description,
|
||||||
|
keywords: SEO_CONFIG.discover.keywords,
|
||||||
|
alternates: {
|
||||||
|
canonical: `${SITE_URL}/discover`,
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: SEO_CONFIG.discover.title,
|
||||||
|
description: SEO_CONFIG.discover.description,
|
||||||
|
url: `${SITE_URL}/discover`,
|
||||||
|
siteName: 'Pounce',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${SITE_URL}/og-discover.png`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'TLD Pricing & Trends - Pounce',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: 'en_US',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: SEO_CONFIG.discover.title,
|
||||||
|
description: SEO_CONFIG.discover.description,
|
||||||
|
images: [`${SITE_URL}/og-discover.png`],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DiscoverLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const webPageSchema = generateWebPageSchema({
|
||||||
|
title: SEO_CONFIG.discover.title,
|
||||||
|
description: SEO_CONFIG.discover.description,
|
||||||
|
url: `${SITE_URL}/discover`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const breadcrumbSchema = generateBreadcrumbSchema([
|
||||||
|
{ name: 'Home', url: SITE_URL },
|
||||||
|
{ name: 'TLD Intelligence', url: `${SITE_URL}/discover` },
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Script
|
||||||
|
id="discover-webpage-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(webPageSchema) }}
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="discover-breadcrumb-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ -5,7 +5,7 @@ import Script from 'next/script'
|
|||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.com'
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.ch'
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
width: 'device-width',
|
width: 'device-width',
|
||||||
@ -16,25 +16,34 @@ export const viewport: Viewport = {
|
|||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
metadataBase: new URL(siteUrl),
|
metadataBase: new URL(siteUrl),
|
||||||
title: {
|
title: {
|
||||||
default: 'Pounce - Domain Intelligence for Investors',
|
default: 'Pounce - Domain Intelligence Platform | Find, Track & Trade Domains',
|
||||||
template: '%s | Pounce',
|
template: '%s | Pounce',
|
||||||
},
|
},
|
||||||
description: 'The market never sleeps. You should. Scan, track, and trade domains with real-time drops, auctions, and TLD price intelligence. Spam-filtered. 0% commission.',
|
description: 'The #1 domain intelligence platform. Real-time auction aggregation from GoDaddy, Sedo, DropCatch & more. TLD price tracking, spam-free market feed, and portfolio management. Find undervalued domains before anyone else.',
|
||||||
keywords: [
|
keywords: [
|
||||||
'domain marketplace',
|
|
||||||
'domain auctions',
|
|
||||||
'TLD pricing',
|
|
||||||
'domain investing',
|
|
||||||
'expired domains',
|
|
||||||
'domain intelligence',
|
'domain intelligence',
|
||||||
|
'domain auctions',
|
||||||
|
'expired domains',
|
||||||
|
'domain investing',
|
||||||
|
'TLD pricing',
|
||||||
'domain drops',
|
'domain drops',
|
||||||
'premium domains',
|
'domain marketplace',
|
||||||
'domain monitoring',
|
'domain monitoring',
|
||||||
|
'domain portfolio',
|
||||||
'domain valuation',
|
'domain valuation',
|
||||||
'domain market analysis',
|
'premium domains',
|
||||||
|
'domain flipping',
|
||||||
|
'godaddy auctions',
|
||||||
|
'sedo marketplace',
|
||||||
|
'dropcatch',
|
||||||
|
'namejet',
|
||||||
|
'domain trading',
|
||||||
'buy domains',
|
'buy domains',
|
||||||
'sell domains',
|
'sell domains',
|
||||||
'domain portfolio',
|
'domain price trends',
|
||||||
|
'.com domains',
|
||||||
|
'.io domains',
|
||||||
|
'.ai domains',
|
||||||
],
|
],
|
||||||
authors: [{ name: 'Pounce' }],
|
authors: [{ name: 'Pounce' }],
|
||||||
creator: 'Pounce',
|
creator: 'Pounce',
|
||||||
@ -44,29 +53,39 @@ export const metadata: Metadata = {
|
|||||||
address: false,
|
address: false,
|
||||||
telephone: false,
|
telephone: false,
|
||||||
},
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: siteUrl,
|
||||||
|
languages: {
|
||||||
|
'en': siteUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: 'website',
|
type: 'website',
|
||||||
locale: 'en_US',
|
locale: 'en_US',
|
||||||
url: siteUrl,
|
url: siteUrl,
|
||||||
siteName: 'Pounce',
|
siteName: 'Pounce',
|
||||||
title: 'Pounce - Domain Intelligence for Investors',
|
title: 'Pounce - Domain Intelligence Platform | Find, Track & Trade Domains',
|
||||||
description: 'The market never sleeps. You should. Real-time domain drops, auctions, and TLD price intelligence.',
|
description: 'The #1 domain intelligence platform. Real-time auction aggregation, TLD price tracking, spam-free market feed. Find undervalued domains before anyone else.',
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: `${siteUrl}/og-image.png`,
|
url: `${siteUrl}/og-image.png`,
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
alt: 'Pounce - Domain Intelligence',
|
alt: 'Pounce - Domain Intelligence Platform',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
title: 'Pounce - Domain Intelligence for Investors',
|
title: 'Pounce - Domain Intelligence Platform',
|
||||||
description: 'The market never sleeps. You should. Real-time domain drops, auctions, and TLD price intelligence.',
|
description: 'The #1 domain intelligence platform. Real-time auctions, TLD pricing, spam-free feed. Find undervalued domains.',
|
||||||
creator: '@pouncedomains',
|
creator: '@pouncedomains',
|
||||||
|
site: '@pouncedomains',
|
||||||
images: [`${siteUrl}/og-image.png`],
|
images: [`${siteUrl}/og-image.png`],
|
||||||
},
|
},
|
||||||
|
verification: {
|
||||||
|
google: 'YOUR_GOOGLE_VERIFICATION_CODE', // Add your Google Search Console verification
|
||||||
|
},
|
||||||
robots: {
|
robots: {
|
||||||
index: true,
|
index: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
|
|||||||
103
frontend/src/app/pricing/layout.tsx
Normal file
103
frontend/src/app/pricing/layout.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
import Script from 'next/script'
|
||||||
|
import { SEO_CONFIG, SITE_URL, generateWebPageSchema, generateBreadcrumbSchema, generateSoftwareApplicationSchema, generateFAQSchema } from '@/lib/seo'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: SEO_CONFIG.pricing.title,
|
||||||
|
description: SEO_CONFIG.pricing.description,
|
||||||
|
keywords: SEO_CONFIG.pricing.keywords,
|
||||||
|
alternates: {
|
||||||
|
canonical: `${SITE_URL}/pricing`,
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: SEO_CONFIG.pricing.title,
|
||||||
|
description: SEO_CONFIG.pricing.description,
|
||||||
|
url: `${SITE_URL}/pricing`,
|
||||||
|
siteName: 'Pounce',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${SITE_URL}/og-pricing.png`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Pricing Plans - Pounce',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: 'en_US',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: SEO_CONFIG.pricing.title,
|
||||||
|
description: SEO_CONFIG.pricing.description,
|
||||||
|
images: [`${SITE_URL}/og-pricing.png`],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PricingLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const webPageSchema = generateWebPageSchema({
|
||||||
|
title: SEO_CONFIG.pricing.title,
|
||||||
|
description: SEO_CONFIG.pricing.description,
|
||||||
|
url: `${SITE_URL}/pricing`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const breadcrumbSchema = generateBreadcrumbSchema([
|
||||||
|
{ name: 'Home', url: SITE_URL },
|
||||||
|
{ name: 'Pricing', url: `${SITE_URL}/pricing` },
|
||||||
|
])
|
||||||
|
|
||||||
|
const softwareSchema = generateSoftwareApplicationSchema()
|
||||||
|
|
||||||
|
const faqSchema = generateFAQSchema([
|
||||||
|
{
|
||||||
|
question: 'Is there a free plan?',
|
||||||
|
answer: 'Yes! Scout is completely free forever. You get access to the raw market feed, 5 watchlist domains, and basic TLD intelligence.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'What payment methods do you accept?',
|
||||||
|
answer: 'We accept all major credit cards (Visa, Mastercard, Amex) through Stripe. Payments are processed securely.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Can I cancel anytime?',
|
||||||
|
answer: 'Absolutely. No contracts, no hidden fees. Cancel your subscription anytime from your account settings.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'What is the Pounce Score?',
|
||||||
|
answer: 'The Pounce Score is our proprietary domain valuation metric that considers length, TLD, keywords, brandability, and market comparables.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Do you charge commission on sales?',
|
||||||
|
answer: 'No! Unlike other platforms that charge 10-20% commission, Pounce Direct marketplace has 0% commission. You keep 100% of your sale price.',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Script
|
||||||
|
id="pricing-webpage-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(webPageSchema) }}
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="pricing-breadcrumb-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="pricing-software-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(softwareSchema) }}
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="pricing-faq-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
34
frontend/src/app/robots.ts
Normal file
34
frontend/src/app/robots.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { MetadataRoute } from 'next'
|
||||||
|
import { SITE_URL } from '@/lib/seo'
|
||||||
|
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
return {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
userAgent: '*',
|
||||||
|
allow: '/',
|
||||||
|
disallow: [
|
||||||
|
'/terminal/',
|
||||||
|
'/api/',
|
||||||
|
'/admin/',
|
||||||
|
'/command/',
|
||||||
|
'/_next/',
|
||||||
|
'/static/',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userAgent: 'Googlebot',
|
||||||
|
allow: '/',
|
||||||
|
disallow: [
|
||||||
|
'/terminal/',
|
||||||
|
'/api/',
|
||||||
|
'/admin/',
|
||||||
|
'/command/',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sitemap: `${SITE_URL}/sitemap.xml`,
|
||||||
|
host: SITE_URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,74 +1,102 @@
|
|||||||
import { MetadataRoute } from 'next'
|
import { MetadataRoute } from 'next'
|
||||||
|
import { POPULAR_TLDS, SITE_URL } from '@/lib/seo'
|
||||||
|
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.com'
|
// This generates a dynamic sitemap including all TLD pages
|
||||||
|
|
||||||
// Top TLDs to include in sitemap (programmatic SEO)
|
|
||||||
const TOP_TLDS = [
|
|
||||||
'com', 'net', 'org', 'io', 'ai', 'co', 'app', 'dev', 'xyz', 'online',
|
|
||||||
'tech', 'store', 'site', 'cloud', 'pro', 'info', 'biz', 'me', 'tv', 'cc',
|
|
||||||
'de', 'uk', 'eu', 'us', 'ca', 'au', 'jp', 'fr', 'es', 'it',
|
|
||||||
'ch', 'nl', 'se', 'no', 'dk', 'fi', 'at', 'be', 'pl', 'cz',
|
|
||||||
'web', 'digital', 'domains', 'blog', 'shop', 'news', 'email', 'services',
|
|
||||||
'consulting', 'agency', 'studio', 'media', 'design', 'art', 'photo', 'video',
|
|
||||||
'crypto', 'nft', 'dao', 'defi', 'web3', 'metaverse', 'blockchain', 'bitcoin',
|
|
||||||
'finance', 'bank', 'invest', 'trading', 'market', 'fund', 'capital', 'ventures',
|
|
||||||
'legal', 'law', 'attorney', 'lawyer', 'consulting', 'tax', 'insurance', 'realty',
|
|
||||||
'education', 'university', 'college', 'school', 'academy', 'training', 'courses',
|
|
||||||
'health', 'medical', 'dental', 'clinic', 'doctor', 'care', 'fitness', 'wellness',
|
|
||||||
'food', 'restaurant', 'cafe', 'bar', 'pizza', 'delivery', 'recipes', 'cooking',
|
|
||||||
'travel', 'hotel', 'flights', 'tours', 'vacation', 'cruise', 'booking', 'tickets',
|
|
||||||
'games', 'gaming', 'play', 'casino', 'bet', 'poker', 'sports', 'esports',
|
|
||||||
'fashion', 'clothing', 'beauty', 'style', 'jewelry', 'watches', 'luxury', 'boutique',
|
|
||||||
]
|
|
||||||
|
|
||||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
const routes: MetadataRoute.Sitemap = [
|
const baseUrl = SITE_URL
|
||||||
// Main pages
|
const now = new Date()
|
||||||
|
|
||||||
|
// Static pages with high priority
|
||||||
|
const staticPages: MetadataRoute.Sitemap = [
|
||||||
{
|
{
|
||||||
url: siteUrl,
|
url: baseUrl,
|
||||||
lastModified: new Date(),
|
lastModified: now,
|
||||||
changeFrequency: 'daily',
|
changeFrequency: 'daily',
|
||||||
priority: 1.0,
|
priority: 1.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: `${siteUrl}/market`,
|
url: `${baseUrl}/discover`,
|
||||||
lastModified: new Date(),
|
lastModified: now,
|
||||||
changeFrequency: 'hourly',
|
|
||||||
priority: 0.9,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: `${siteUrl}/discover`,
|
|
||||||
lastModified: new Date(),
|
|
||||||
changeFrequency: 'daily',
|
changeFrequency: 'daily',
|
||||||
priority: 0.9,
|
priority: 0.9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: `${siteUrl}/pricing`,
|
url: `${baseUrl}/acquire`,
|
||||||
lastModified: new Date(),
|
lastModified: now,
|
||||||
|
changeFrequency: 'hourly',
|
||||||
|
priority: 0.9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/yield`,
|
||||||
|
lastModified: now,
|
||||||
changeFrequency: 'weekly',
|
changeFrequency: 'weekly',
|
||||||
priority: 0.8,
|
priority: 0.8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: `${siteUrl}/about`,
|
url: `${baseUrl}/pricing`,
|
||||||
lastModified: new Date(),
|
lastModified: now,
|
||||||
|
changeFrequency: 'weekly',
|
||||||
|
priority: 0.8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/about`,
|
||||||
|
lastModified: now,
|
||||||
changeFrequency: 'monthly',
|
changeFrequency: 'monthly',
|
||||||
priority: 0.5,
|
priority: 0.5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: `${siteUrl}/contact`,
|
url: `${baseUrl}/blog`,
|
||||||
lastModified: new Date(),
|
lastModified: now,
|
||||||
|
changeFrequency: 'weekly',
|
||||||
|
priority: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/login`,
|
||||||
|
lastModified: now,
|
||||||
changeFrequency: 'monthly',
|
changeFrequency: 'monthly',
|
||||||
priority: 0.5,
|
priority: 0.3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/register`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 0.4,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// Add TLD pages (programmatic SEO - high priority for search)
|
// Fetch all TLDs from API for dynamic TLD pages
|
||||||
const tldPages: MetadataRoute.Sitemap = TOP_TLDS.map((tld) => ({
|
let tldPages: MetadataRoute.Sitemap = []
|
||||||
url: `${siteUrl}/discover/${tld}`,
|
|
||||||
lastModified: new Date(),
|
try {
|
||||||
changeFrequency: 'daily',
|
// Try to fetch TLDs from the API
|
||||||
priority: 0.8,
|
const response = await fetch(`${baseUrl}/api/v1/tld/overview?limit=100`, {
|
||||||
}))
|
next: { revalidate: 86400 }, // Revalidate daily
|
||||||
|
})
|
||||||
return [...routes, ...tldPages]
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
const tlds = data.tlds || []
|
||||||
|
|
||||||
|
tldPages = tlds.map((tld: { tld: string }) => ({
|
||||||
|
url: `${baseUrl}/tld/${tld.tld.toLowerCase()}`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'daily' as const,
|
||||||
|
priority: 0.7,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch TLDs for sitemap:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If API failed, use popular TLDs as fallback
|
||||||
|
if (tldPages.length === 0) {
|
||||||
|
tldPages = POPULAR_TLDS.map(tld => ({
|
||||||
|
url: `${baseUrl}/tld/${tld}`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'daily' as const,
|
||||||
|
priority: 0.7,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...staticPages, ...tldPages]
|
||||||
}
|
}
|
||||||
|
|||||||
430
frontend/src/app/tld/[tld]/TldDetailClient.tsx
Normal file
430
frontend/src/app/tld/[tld]/TldDetailClient.tsx
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Header } from '@/components/Header'
|
||||||
|
import { Footer } from '@/components/Footer'
|
||||||
|
import { useStore } from '@/lib/store'
|
||||||
|
import { api } from '@/lib/api'
|
||||||
|
import {
|
||||||
|
Globe,
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
Minus,
|
||||||
|
DollarSign,
|
||||||
|
Calendar,
|
||||||
|
Building,
|
||||||
|
AlertTriangle,
|
||||||
|
ExternalLink,
|
||||||
|
ChevronLeft,
|
||||||
|
Lock,
|
||||||
|
ArrowRight,
|
||||||
|
Search,
|
||||||
|
Check,
|
||||||
|
X,
|
||||||
|
Loader2,
|
||||||
|
BarChart3
|
||||||
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
interface TldData {
|
||||||
|
tld: string
|
||||||
|
type: string | null
|
||||||
|
registration_price: number | null
|
||||||
|
renewal_price: number | null
|
||||||
|
registrar_count: number
|
||||||
|
description?: string
|
||||||
|
introduced?: string
|
||||||
|
registry?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PriceHistory {
|
||||||
|
date: string
|
||||||
|
price: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RegistrarPrice {
|
||||||
|
registrar: string
|
||||||
|
registration: number
|
||||||
|
renewal: number
|
||||||
|
transfer: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tld: string
|
||||||
|
initialData: TldData | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TldDetailClient({ tld, initialData }: Props) {
|
||||||
|
const { isAuthenticated, checkAuth, subscription } = useStore()
|
||||||
|
const [data, setData] = useState<TldData | null>(initialData)
|
||||||
|
const [priceHistory, setPriceHistory] = useState<PriceHistory[]>([])
|
||||||
|
const [registrars, setRegistrars] = useState<RegistrarPrice[]>([])
|
||||||
|
const [loading, setLoading] = useState(!initialData)
|
||||||
|
const [checkDomain, setCheckDomain] = useState('')
|
||||||
|
const [checking, setChecking] = useState(false)
|
||||||
|
const [checkResult, setCheckResult] = useState<{ available: boolean; domain: string } | null>(null)
|
||||||
|
|
||||||
|
const tier = subscription?.tier || 'scout'
|
||||||
|
const canSeeRenewal = tier !== 'scout'
|
||||||
|
const canSeeHistory = tier === 'tycoon'
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuth()
|
||||||
|
}, [checkAuth])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTldData()
|
||||||
|
}, [tld])
|
||||||
|
|
||||||
|
const loadTldData = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const [tldInfo, prices] = await Promise.all([
|
||||||
|
api.getTldInfo(tld),
|
||||||
|
api.getTldPrices(tld),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (tldInfo) {
|
||||||
|
setData(tldInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prices) {
|
||||||
|
setRegistrars(prices.registrars || [])
|
||||||
|
setPriceHistory(prices.history || [])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load TLD data:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCheckDomain = async () => {
|
||||||
|
if (!checkDomain.trim()) return
|
||||||
|
setChecking(true)
|
||||||
|
try {
|
||||||
|
const domain = checkDomain.includes('.') ? checkDomain : `${checkDomain}.${tld}`
|
||||||
|
const result = await api.checkDomain(domain)
|
||||||
|
setCheckResult({ available: result.available, domain })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Domain check failed:', error)
|
||||||
|
} finally {
|
||||||
|
setChecking(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRiskLevel = () => {
|
||||||
|
if (!data?.registration_price || !data?.renewal_price) return 'unknown'
|
||||||
|
const ratio = data.renewal_price / data.registration_price
|
||||||
|
if (ratio > 5) return 'high'
|
||||||
|
if (ratio > 2) return 'medium'
|
||||||
|
return 'low'
|
||||||
|
}
|
||||||
|
|
||||||
|
const riskLevel = getRiskLevel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#020202] flex flex-col">
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<main className="flex-1">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="relative pt-24 sm:pt-32 pb-12 sm:pb-16 overflow-hidden">
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:60px_60px]" />
|
||||||
|
|
||||||
|
<div className="relative max-w-6xl mx-auto px-4 sm:px-6">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<Link
|
||||||
|
href="/discover"
|
||||||
|
className="inline-flex items-center gap-2 text-sm text-white/50 hover:text-white transition-colors mb-8"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
<span>Back to TLD Intelligence</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* TLD Header */}
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-8 mb-12">
|
||||||
|
<div className="flex items-start gap-6">
|
||||||
|
<div className="w-20 h-20 bg-accent/10 border border-accent/20 flex items-center justify-center shrink-0">
|
||||||
|
<Globe className="w-10 h-10 text-accent" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-display font-bold text-white tracking-tight">
|
||||||
|
.{tld.toUpperCase()}
|
||||||
|
</h1>
|
||||||
|
<p className="text-white/50 text-lg mt-2">
|
||||||
|
{data?.type || 'Domain Extension'} {data?.registry && `• ${data.registry}`}
|
||||||
|
</p>
|
||||||
|
{data?.description && (
|
||||||
|
<p className="text-white/70 mt-4 max-w-xl">{data.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Risk Badge */}
|
||||||
|
<div className={clsx(
|
||||||
|
"px-6 py-4 border",
|
||||||
|
riskLevel === 'high' && "bg-red-500/10 border-red-500/30",
|
||||||
|
riskLevel === 'medium' && "bg-yellow-500/10 border-yellow-500/30",
|
||||||
|
riskLevel === 'low' && "bg-green-500/10 border-green-500/30",
|
||||||
|
riskLevel === 'unknown' && "bg-white/5 border-white/10"
|
||||||
|
)}>
|
||||||
|
<div className="text-[10px] font-mono uppercase tracking-wider text-white/40 mb-1">
|
||||||
|
Renewal Risk
|
||||||
|
</div>
|
||||||
|
<div className={clsx(
|
||||||
|
"text-2xl font-bold capitalize",
|
||||||
|
riskLevel === 'high' && "text-red-400",
|
||||||
|
riskLevel === 'medium' && "text-yellow-400",
|
||||||
|
riskLevel === 'low' && "text-green-400",
|
||||||
|
riskLevel === 'unknown' && "text-white/50"
|
||||||
|
)}>
|
||||||
|
{riskLevel === 'unknown' ? 'N/A' : riskLevel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-white/[0.02] border border-white/[0.06] p-6">
|
||||||
|
<div className="text-[10px] font-mono uppercase tracking-wider text-white/40 mb-2">
|
||||||
|
Registration
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-white">
|
||||||
|
{data?.registration_price ? `$${data.registration_price.toFixed(2)}` : '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/[0.02] border border-white/[0.06] p-6 relative">
|
||||||
|
<div className="text-[10px] font-mono uppercase tracking-wider text-white/40 mb-2">
|
||||||
|
Renewal
|
||||||
|
</div>
|
||||||
|
{canSeeRenewal ? (
|
||||||
|
<div className="text-2xl font-bold text-white">
|
||||||
|
{data?.renewal_price ? `$${data.renewal_price.toFixed(2)}` : '—'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Lock className="w-4 h-4 text-white/30" />
|
||||||
|
<span className="text-white/30 text-sm">Trader+</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/[0.02] border border-white/[0.06] p-6">
|
||||||
|
<div className="text-[10px] font-mono uppercase tracking-wider text-white/40 mb-2">
|
||||||
|
Registrars
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-white">
|
||||||
|
{data?.registrar_count || registrars.length || '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/[0.02] border border-white/[0.06] p-6">
|
||||||
|
<div className="text-[10px] font-mono uppercase tracking-wider text-white/40 mb-2">
|
||||||
|
Introduced
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-white">
|
||||||
|
{data?.introduced || '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Domain Checker */}
|
||||||
|
<section className="py-12 border-y border-white/[0.06]">
|
||||||
|
<div className="max-w-6xl mx-auto px-4 sm:px-6">
|
||||||
|
<h2 className="text-xl font-bold text-white mb-6">Check .{tld.toUpperCase()} Availability</h2>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={checkDomain}
|
||||||
|
onChange={(e) => setCheckDomain(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleCheckDomain()}
|
||||||
|
placeholder={`yourname.${tld}`}
|
||||||
|
className="w-full bg-white/[0.02] border border-white/[0.08] px-4 py-3 text-white placeholder:text-white/30 focus:outline-none focus:border-accent font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleCheckDomain}
|
||||||
|
disabled={checking}
|
||||||
|
className="px-6 py-3 bg-accent text-black font-bold hover:bg-accent/90 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{checking ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Check'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{checkResult && (
|
||||||
|
<div className={clsx(
|
||||||
|
"mt-4 p-4 border flex items-center gap-4",
|
||||||
|
checkResult.available
|
||||||
|
? "bg-green-500/10 border-green-500/30"
|
||||||
|
: "bg-red-500/10 border-red-500/30"
|
||||||
|
)}>
|
||||||
|
{checkResult.available ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-6 h-6 text-green-400" />
|
||||||
|
<div>
|
||||||
|
<div className="font-bold text-white">{checkResult.domain} is available!</div>
|
||||||
|
<a
|
||||||
|
href={`https://www.namecheap.com/domains/registration/results/?domain=${checkResult.domain}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-accent text-sm hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Register now <ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<X className="w-6 h-6 text-red-400" />
|
||||||
|
<div className="font-bold text-white">{checkResult.domain} is taken</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Price History Chart */}
|
||||||
|
<section className="py-12">
|
||||||
|
<div className="max-w-6xl mx-auto px-4 sm:px-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-xl font-bold text-white">Price History</h2>
|
||||||
|
{!canSeeHistory && (
|
||||||
|
<Link
|
||||||
|
href="/pricing"
|
||||||
|
className="text-sm text-accent hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Unlock with Tycoon <ArrowRight className="w-3 h-3" />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/[0.02] border border-white/[0.06] p-8 min-h-[300px] relative">
|
||||||
|
{canSeeHistory ? (
|
||||||
|
priceHistory.length > 0 ? (
|
||||||
|
<div className="text-white/50 text-center py-12">
|
||||||
|
<BarChart3 className="w-12 h-12 mx-auto mb-4 text-white/20" />
|
||||||
|
Price chart visualization here
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-white/50 text-center py-12">
|
||||||
|
No historical data available yet
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="absolute inset-0 bg-[#020202]/80 backdrop-blur-sm flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Lock className="w-12 h-12 text-white/20 mx-auto mb-4" />
|
||||||
|
<p className="text-white/50 mb-4">Historical data requires Tycoon</p>
|
||||||
|
<Link
|
||||||
|
href="/pricing"
|
||||||
|
className="px-6 py-2 bg-accent text-black font-bold text-sm hover:bg-accent/90 transition-colors inline-block"
|
||||||
|
>
|
||||||
|
Upgrade to Tycoon
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Registrar Comparison */}
|
||||||
|
<section className="py-12 border-t border-white/[0.06]">
|
||||||
|
<div className="max-w-6xl mx-auto px-4 sm:px-6">
|
||||||
|
<h2 className="text-xl font-bold text-white mb-6">Compare Registrars</h2>
|
||||||
|
|
||||||
|
{registrars.length > 0 ? (
|
||||||
|
<div className="border border-white/[0.06] overflow-hidden">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-white/[0.02] border-b border-white/[0.06]">
|
||||||
|
<th className="text-left p-4 text-[10px] font-mono uppercase tracking-wider text-white/40">
|
||||||
|
Registrar
|
||||||
|
</th>
|
||||||
|
<th className="text-right p-4 text-[10px] font-mono uppercase tracking-wider text-white/40">
|
||||||
|
Registration
|
||||||
|
</th>
|
||||||
|
<th className="text-right p-4 text-[10px] font-mono uppercase tracking-wider text-white/40">
|
||||||
|
Renewal
|
||||||
|
</th>
|
||||||
|
<th className="text-right p-4 text-[10px] font-mono uppercase tracking-wider text-white/40">
|
||||||
|
Transfer
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{registrars.map((reg, i) => (
|
||||||
|
<tr
|
||||||
|
key={reg.registrar}
|
||||||
|
className={clsx(
|
||||||
|
"border-b border-white/[0.06] hover:bg-white/[0.02] transition-colors",
|
||||||
|
i === 0 && "bg-accent/5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{i === 0 && (
|
||||||
|
<span className="text-[9px] font-mono uppercase bg-accent/20 text-accent px-2 py-0.5">
|
||||||
|
Best
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="font-medium text-white">{reg.registrar}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-right font-mono text-white">
|
||||||
|
${reg.registration.toFixed(2)}
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-right font-mono text-white">
|
||||||
|
{canSeeRenewal ? `$${reg.renewal.toFixed(2)}` : (
|
||||||
|
<span className="text-white/30">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-right font-mono text-white">
|
||||||
|
${reg.transfer.toFixed(2)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white/[0.02] border border-white/[0.06] p-12 text-center">
|
||||||
|
<Building className="w-12 h-12 text-white/20 mx-auto mb-4" />
|
||||||
|
<p className="text-white/50">No registrar data available</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="py-16 border-t border-white/[0.06]">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 text-center">
|
||||||
|
<h2 className="text-3xl font-bold text-white mb-4">
|
||||||
|
Track .{tld.toUpperCase()} Prices
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/60 mb-8 max-w-xl mx-auto">
|
||||||
|
Get alerts when .{tld.toUpperCase()} prices change. Monitor domains and never miss a deal.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/register"
|
||||||
|
className="inline-flex items-center gap-2 px-8 py-4 bg-accent text-black font-bold hover:bg-accent/90 transition-colors"
|
||||||
|
>
|
||||||
|
Start Free <ArrowRight className="w-4 h-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
176
frontend/src/app/tld/[tld]/page.tsx
Normal file
176
frontend/src/app/tld/[tld]/page.tsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
import Script from 'next/script'
|
||||||
|
import { SITE_URL, POPULAR_TLDS, generateTLDPageSchema, generateBreadcrumbSchema } from '@/lib/seo'
|
||||||
|
import TldDetailClient from './TldDetailClient'
|
||||||
|
|
||||||
|
interface TldData {
|
||||||
|
tld: string
|
||||||
|
type: string | null
|
||||||
|
registration_price: number | null
|
||||||
|
renewal_price: number | null
|
||||||
|
registrar_count: number
|
||||||
|
description?: string
|
||||||
|
introduced?: string
|
||||||
|
registry?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTldData(tld: string): Promise<TldData | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${SITE_URL}/api/v1/tld/${tld}`, {
|
||||||
|
next: { revalidate: 3600 }, // Revalidate every hour
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch TLD data:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate static params for popular TLDs
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
return POPULAR_TLDS.map(tld => ({ tld }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate dynamic metadata for each TLD
|
||||||
|
export async function generateMetadata({
|
||||||
|
params
|
||||||
|
}: {
|
||||||
|
params: Promise<{ tld: string }>
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const { tld } = await params
|
||||||
|
const tldUpper = tld.toUpperCase()
|
||||||
|
const tldLower = tld.toLowerCase()
|
||||||
|
|
||||||
|
// Try to fetch real data for better SEO
|
||||||
|
const data = await getTldData(tldLower)
|
||||||
|
|
||||||
|
const priceInfo = data?.registration_price
|
||||||
|
? `from $${data.registration_price}`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const typeInfo = data?.type
|
||||||
|
? `(${data.type})`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const title = `.${tldUpper} Domain Pricing & Trends 2025 | Registration & Renewal Costs | Pounce`
|
||||||
|
const description = `Complete .${tldUpper} domain guide ${typeInfo}. Current registration ${priceInfo}, renewal prices, registrar comparison, and historical trends. Find the cheapest .${tldLower} domains.`
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
keywords: [
|
||||||
|
`.${tldLower} domain`, `.${tldLower} price`, `.${tldLower} registration`,
|
||||||
|
`.${tldLower} renewal cost`, `.${tldLower} domain buy`, `${tldLower} domain extension`,
|
||||||
|
`.${tldLower} registrar`, `cheap .${tldLower} domain`, `.${tldLower} 2025`,
|
||||||
|
`${tldLower} domain price history`, `${tldLower} domain trends`
|
||||||
|
],
|
||||||
|
alternates: {
|
||||||
|
canonical: `${SITE_URL}/tld/${tldLower}`,
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
url: `${SITE_URL}/tld/${tldLower}`,
|
||||||
|
siteName: 'Pounce',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${SITE_URL}/og-tld.png`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: `.${tldUpper} Domain Pricing - Pounce`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: 'en_US',
|
||||||
|
type: 'article',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
images: [`${SITE_URL}/og-tld.png`],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function TldPage({
|
||||||
|
params
|
||||||
|
}: {
|
||||||
|
params: Promise<{ tld: string }>
|
||||||
|
}) {
|
||||||
|
const { tld } = await params
|
||||||
|
const tldLower = tld.toLowerCase()
|
||||||
|
const tldUpper = tld.toUpperCase()
|
||||||
|
|
||||||
|
// Fetch TLD data for schema
|
||||||
|
const data = await getTldData(tldLower)
|
||||||
|
|
||||||
|
// Generate structured data
|
||||||
|
const tldSchema = generateTLDPageSchema(tldLower, {
|
||||||
|
registrationPrice: data?.registration_price || undefined,
|
||||||
|
renewalPrice: data?.renewal_price || undefined,
|
||||||
|
type: data?.type || undefined,
|
||||||
|
registrarCount: data?.registrar_count || 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const breadcrumbSchema = generateBreadcrumbSchema([
|
||||||
|
{ name: 'Home', url: SITE_URL },
|
||||||
|
{ name: 'TLD Intelligence', url: `${SITE_URL}/discover` },
|
||||||
|
{ name: `.${tldUpper}`, url: `${SITE_URL}/tld/${tldLower}` },
|
||||||
|
])
|
||||||
|
|
||||||
|
// Article schema for better SEO
|
||||||
|
const articleSchema = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Article',
|
||||||
|
headline: `.${tldUpper} Domain Pricing & Trends 2025`,
|
||||||
|
description: `Complete guide to .${tldUpper} domain registration and renewal costs`,
|
||||||
|
url: `${SITE_URL}/tld/${tldLower}`,
|
||||||
|
datePublished: '2024-01-01',
|
||||||
|
dateModified: new Date().toISOString().split('T')[0],
|
||||||
|
author: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: 'Pounce',
|
||||||
|
url: SITE_URL,
|
||||||
|
},
|
||||||
|
publisher: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: 'Pounce',
|
||||||
|
logo: {
|
||||||
|
'@type': 'ImageObject',
|
||||||
|
url: `${SITE_URL}/pounce-logo.png`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mainEntityOfPage: {
|
||||||
|
'@type': 'WebPage',
|
||||||
|
'@id': `${SITE_URL}/tld/${tldLower}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Script
|
||||||
|
id="tld-product-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(tldSchema) }}
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="tld-breadcrumb-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="tld-article-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
|
||||||
|
/>
|
||||||
|
<TldDetailClient tld={tldLower} initialData={data} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
92
frontend/src/app/yield/layout.tsx
Normal file
92
frontend/src/app/yield/layout.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
import Script from 'next/script'
|
||||||
|
import { SEO_CONFIG, SITE_URL, generateWebPageSchema, generateBreadcrumbSchema, generateFAQSchema } from '@/lib/seo'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: SEO_CONFIG.yield.title,
|
||||||
|
description: SEO_CONFIG.yield.description,
|
||||||
|
keywords: SEO_CONFIG.yield.keywords,
|
||||||
|
alternates: {
|
||||||
|
canonical: `${SITE_URL}/yield`,
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: SEO_CONFIG.yield.title,
|
||||||
|
description: SEO_CONFIG.yield.description,
|
||||||
|
url: `${SITE_URL}/yield`,
|
||||||
|
siteName: 'Pounce',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${SITE_URL}/og-yield.png`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Domain Monetization - Pounce Yield',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: 'en_US',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: SEO_CONFIG.yield.title,
|
||||||
|
description: SEO_CONFIG.yield.description,
|
||||||
|
images: [`${SITE_URL}/og-yield.png`],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function YieldLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const webPageSchema = generateWebPageSchema({
|
||||||
|
title: SEO_CONFIG.yield.title,
|
||||||
|
description: SEO_CONFIG.yield.description,
|
||||||
|
url: `${SITE_URL}/yield`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const breadcrumbSchema = generateBreadcrumbSchema([
|
||||||
|
{ name: 'Home', url: SITE_URL },
|
||||||
|
{ name: 'Domain Yield', url: `${SITE_URL}/yield` },
|
||||||
|
])
|
||||||
|
|
||||||
|
const faqSchema = generateFAQSchema([
|
||||||
|
{
|
||||||
|
question: 'What is Pounce Yield?',
|
||||||
|
answer: 'Pounce Yield is an AI-powered domain monetization system that turns parked domains into revenue-generating assets through intent routing, not traditional parking ads.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'How much can I earn with Pounce Yield?',
|
||||||
|
answer: 'Earnings depend on your domain\'s traffic and intent. High-intent domains in verticals like finance or software can earn $10-50+ per conversion, compared to pennies from traditional parking.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'What is intent routing?',
|
||||||
|
answer: 'Intent routing analyzes what visitors to your domain are looking for and routes them to relevant affiliate offers or services, maximizing conversion value.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'What is the revenue share?',
|
||||||
|
answer: 'Trader members receive 70% of all affiliate revenue generated. Tycoon members get priority routing to highest-paying partners.',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Script
|
||||||
|
id="yield-webpage-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(webPageSchema) }}
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="yield-breadcrumb-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="yield-faq-schema"
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ -555,6 +555,46 @@ class ApiClient {
|
|||||||
}>('/tld-prices/trending')
|
}>('/tld-prices/trending')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get TLD info for SEO pages
|
||||||
|
async getTldInfo(tld: string) {
|
||||||
|
try {
|
||||||
|
const compare = await this.getTldCompare(tld)
|
||||||
|
return {
|
||||||
|
tld: compare.tld,
|
||||||
|
type: compare.type,
|
||||||
|
registration_price: compare.cheapest_price,
|
||||||
|
renewal_price: compare.registrars?.[0]?.renewal_price || null,
|
||||||
|
registrar_count: compare.registrars?.length || 0,
|
||||||
|
description: compare.description,
|
||||||
|
introduced: compare.introduced?.toString() || null,
|
||||||
|
registry: compare.registry,
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get TLD prices for SEO pages
|
||||||
|
async getTldPrices(tld: string) {
|
||||||
|
try {
|
||||||
|
const [compare, history] = await Promise.all([
|
||||||
|
this.getTldCompare(tld),
|
||||||
|
this.getTldHistory(tld, 365),
|
||||||
|
])
|
||||||
|
return {
|
||||||
|
registrars: compare.registrars?.map(r => ({
|
||||||
|
registrar: r.name,
|
||||||
|
registration: r.registration_price,
|
||||||
|
renewal: r.renewal_price,
|
||||||
|
transfer: r.transfer_price,
|
||||||
|
})) || [],
|
||||||
|
history: history.history || [],
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { registrars: [], history: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============== Portfolio ==============
|
// ============== Portfolio ==============
|
||||||
|
|
||||||
async getPortfolio(
|
async getPortfolio(
|
||||||
|
|||||||
@ -1,202 +1,147 @@
|
|||||||
/**
|
// SEO Configuration for Pounce
|
||||||
* SEO & Geo-targeting utilities
|
// Centralized SEO metadata and structured data
|
||||||
*/
|
|
||||||
|
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.com'
|
export const SITE_URL = 'https://pounce.ch'
|
||||||
|
export const SITE_NAME = 'Pounce'
|
||||||
|
export const DEFAULT_OG_IMAGE = `${SITE_URL}/og-image.png`
|
||||||
|
|
||||||
/**
|
export const SEO_CONFIG = {
|
||||||
* Supported locales for geo-targeting
|
home: {
|
||||||
*/
|
title: 'Pounce - Domain Intelligence Platform | Find, Track & Trade Domains',
|
||||||
export const SUPPORTED_LOCALES = {
|
description: 'The #1 domain intelligence platform. Real-time auction aggregation, TLD price tracking, spam-free market feed, and portfolio management. Find undervalued domains before anyone else.',
|
||||||
'en-US': { name: 'English (US)', currency: 'USD', flag: '🇺🇸' },
|
keywords: [
|
||||||
'en-GB': { name: 'English (UK)', currency: 'GBP', flag: '🇬🇧' },
|
'domain intelligence', 'domain auctions', 'expired domains', 'domain investing',
|
||||||
'en-CA': { name: 'English (Canada)', currency: 'CAD', flag: '🇨🇦' },
|
'TLD pricing', 'domain drops', 'domain marketplace', 'domain monitoring',
|
||||||
'en-AU': { name: 'English (Australia)', currency: 'AUD', flag: '🇦🇺' },
|
'domain portfolio', 'domain valuation', 'premium domains', 'domain flipping',
|
||||||
'de-DE': { name: 'Deutsch', currency: 'EUR', flag: '🇩🇪' },
|
'godaddy auctions', 'sedo marketplace', 'dropcatch', 'namejet', 'domain trading'
|
||||||
'de-CH': { name: 'Deutsch (Schweiz)', currency: 'CHF', flag: '🇨🇭' },
|
],
|
||||||
'fr-FR': { name: 'Français', currency: 'EUR', flag: '🇫🇷' },
|
},
|
||||||
'es-ES': { name: 'Español', currency: 'EUR', flag: '🇪🇸' },
|
discover: {
|
||||||
'it-IT': { name: 'Italiano', currency: 'EUR', flag: '🇮🇹' },
|
title: 'TLD Pricing & Trends 2025 | Compare 800+ Domain Extensions | Pounce',
|
||||||
'nl-NL': { name: 'Nederlands', currency: 'EUR', flag: '🇳🇱' },
|
description: 'Compare registration and renewal prices for 800+ TLDs. Track price trends, find hidden renewal traps, and discover the best domain extensions for your investment. Updated daily.',
|
||||||
'pt-BR': { name: 'Português (Brasil)', currency: 'BRL', flag: '🇧🇷' },
|
keywords: [
|
||||||
'ja-JP': { name: '日本語', currency: 'JPY', flag: '🇯🇵' },
|
'TLD pricing', 'domain extension prices', 'TLD comparison', 'domain renewal costs',
|
||||||
'zh-CN': { name: '简体中文', currency: 'CNY', flag: '🇨🇳' },
|
'cheapest TLDs', 'new gTLDs', 'ccTLD pricing', '.com price', '.io domain cost',
|
||||||
} as const
|
'.ai domain price', 'domain registration cost', 'TLD trends 2025'
|
||||||
|
],
|
||||||
export type Locale = keyof typeof SUPPORTED_LOCALES
|
},
|
||||||
|
acquire: {
|
||||||
/**
|
title: 'Domain Auctions & Marketplace | Live Drops & Deals | Pounce',
|
||||||
* Generate hreflang alternates for a page
|
description: 'Browse 10,000+ live domain auctions from GoDaddy, Sedo, DropCatch & more. Spam-filtered feed, real-time updates, and exclusive Pounce Direct listings with 0% commission.',
|
||||||
*/
|
keywords: [
|
||||||
export function generateHreflangAlternates(path: string, currentLocale: Locale = 'en-US') {
|
'domain auctions', 'buy domains', 'expired domain auctions', 'domain marketplace',
|
||||||
const alternates = Object.keys(SUPPORTED_LOCALES).map((locale) => ({
|
'godaddy auctions', 'sedo domains', 'dropcatch', 'namejet auctions',
|
||||||
hreflang: locale,
|
'premium domains for sale', 'domain deals', 'cheap domains'
|
||||||
href: `${siteUrl}/${locale === 'en-US' ? '' : locale}${path}`,
|
],
|
||||||
}))
|
},
|
||||||
|
yield: {
|
||||||
// Add x-default
|
title: 'Domain Monetization | Turn Parked Domains into Revenue | Pounce Yield',
|
||||||
alternates.push({
|
description: 'Stop losing money on parked domains. Pounce Yield uses AI-powered intent routing to monetize your unused domains with up to 70% revenue share. No ads, real affiliate revenue.',
|
||||||
hreflang: 'x-default',
|
keywords: [
|
||||||
href: `${siteUrl}${path}`,
|
'domain monetization', 'parked domain income', 'domain parking alternative',
|
||||||
})
|
'domain revenue', 'affiliate domains', 'intent routing', 'domain yield',
|
||||||
|
'passive domain income', 'monetize parked domains'
|
||||||
return alternates
|
],
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
title: 'Pricing Plans | Start Free, Scale Smart | Pounce',
|
||||||
|
description: 'From hobbyist to tycoon. Scout (free), Trader ($9/mo), Tycoon ($29/mo). Real-time alerts, spam-free feeds, domain valuation, and 0% marketplace commission.',
|
||||||
|
keywords: [
|
||||||
|
'domain tool pricing', 'domain software', 'domain investing tools',
|
||||||
|
'domain monitoring service', 'domain alert service', 'domain trading platform'
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Schema.org structured data generators
|
||||||
* Detect user's preferred locale from headers
|
export function generateOrganizationSchema() {
|
||||||
*/
|
return {
|
||||||
export function detectLocale(acceptLanguage: string | null): Locale {
|
'@context': 'https://schema.org',
|
||||||
if (!acceptLanguage) return 'en-US'
|
'@type': 'Organization',
|
||||||
|
name: 'Pounce',
|
||||||
const languages = acceptLanguage.split(',').map((lang) => {
|
url: SITE_URL,
|
||||||
const [code, q = '1'] = lang.trim().split(';q=')
|
logo: `${SITE_URL}/pounce-logo.png`,
|
||||||
return { code: code.toLowerCase(), quality: parseFloat(q) }
|
description: 'Domain intelligence platform for investors and traders',
|
||||||
})
|
foundingDate: '2024',
|
||||||
|
sameAs: [
|
||||||
// Sort by quality
|
'https://twitter.com/pouncedomains',
|
||||||
languages.sort((a, b) => b.quality - a.quality)
|
'https://github.com/pounce',
|
||||||
|
],
|
||||||
// Find first supported locale
|
contactPoint: {
|
||||||
for (const lang of languages) {
|
'@type': 'ContactPoint',
|
||||||
const locale = Object.keys(SUPPORTED_LOCALES).find((l) =>
|
email: 'hello@pounce.ch',
|
||||||
l.toLowerCase().startsWith(lang.code)
|
contactType: 'Customer Service',
|
||||||
)
|
},
|
||||||
if (locale) return locale as Locale
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'en-US'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format price for locale
|
|
||||||
*/
|
|
||||||
export function formatPrice(amount: number, locale: Locale = 'en-US'): string {
|
|
||||||
const { currency } = SUPPORTED_LOCALES[locale]
|
|
||||||
return new Intl.NumberFormat(locale, {
|
|
||||||
style: 'currency',
|
|
||||||
currency,
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate canonical URL
|
|
||||||
*/
|
|
||||||
export function getCanonicalUrl(path: string, locale?: Locale): string {
|
|
||||||
if (!locale || locale === 'en-US') {
|
|
||||||
return `${siteUrl}${path}`
|
|
||||||
}
|
|
||||||
return `${siteUrl}/${locale}${path}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate page title with branding
|
|
||||||
*/
|
|
||||||
export function generateTitle(title: string, includesBrand: boolean = false): string {
|
|
||||||
return includesBrand ? title : `${title} | Pounce`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Truncate description for meta tags
|
|
||||||
*/
|
|
||||||
export function truncateDescription(text: string, maxLength: number = 160): string {
|
|
||||||
if (text.length <= maxLength) return text
|
|
||||||
return text.slice(0, maxLength - 3) + '...'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate keywords array from string or array
|
|
||||||
*/
|
|
||||||
export function generateKeywords(keywords: string | string[]): string[] {
|
|
||||||
if (Array.isArray(keywords)) return keywords
|
|
||||||
return keywords.split(',').map((k) => k.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performance: Generate preload links for critical resources
|
|
||||||
*/
|
|
||||||
export function getPreloadLinks() {
|
|
||||||
return [
|
|
||||||
{ rel: 'preload', href: '/fonts/inter.woff2', as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' },
|
|
||||||
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
|
||||||
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOrigin: 'anonymous' },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate Open Graph image URL with dynamic content
|
|
||||||
*/
|
|
||||||
export function generateOGImageUrl(params: {
|
|
||||||
title?: string
|
|
||||||
subtitle?: string
|
|
||||||
type?: 'default' | 'tld' | 'domain' | 'market'
|
|
||||||
}): string {
|
|
||||||
const searchParams = new URLSearchParams()
|
|
||||||
if (params.title) searchParams.set('title', params.title)
|
|
||||||
if (params.subtitle) searchParams.set('subtitle', params.subtitle)
|
|
||||||
if (params.type) searchParams.set('type', params.type)
|
|
||||||
|
|
||||||
return `${siteUrl}/api/og?${searchParams.toString()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SEO-friendly slug generator
|
|
||||||
*/
|
|
||||||
export function generateSlug(text: string): string {
|
|
||||||
return text
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^\w\s-]/g, '') // Remove special chars
|
|
||||||
.replace(/\s+/g, '-') // Spaces to hyphens
|
|
||||||
.replace(/-+/g, '-') // Multiple hyphens to single
|
|
||||||
.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract domain from URL for canonical
|
|
||||||
*/
|
|
||||||
export function extractDomain(url: string): string {
|
|
||||||
try {
|
|
||||||
const parsed = new URL(url)
|
|
||||||
return parsed.hostname.replace('www.', '')
|
|
||||||
} catch {
|
|
||||||
return url
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function generateSoftwareApplicationSchema() {
|
||||||
* Check if URL is external
|
return {
|
||||||
*/
|
'@context': 'https://schema.org',
|
||||||
export function isExternalUrl(url: string): boolean {
|
'@type': 'SoftwareApplication',
|
||||||
try {
|
name: 'Pounce Domain Intelligence',
|
||||||
const parsed = new URL(url)
|
applicationCategory: 'BusinessApplication',
|
||||||
return parsed.hostname !== extractDomain(siteUrl)
|
operatingSystem: 'Web',
|
||||||
} catch {
|
url: SITE_URL,
|
||||||
return false
|
description: 'Real-time domain auction aggregation, TLD price tracking, and portfolio management platform',
|
||||||
|
offers: [
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
name: 'Scout',
|
||||||
|
price: '0',
|
||||||
|
priceCurrency: 'USD',
|
||||||
|
description: 'Free tier with basic features',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
name: 'Trader',
|
||||||
|
price: '9',
|
||||||
|
priceCurrency: 'USD',
|
||||||
|
priceValidUntil: '2025-12-31',
|
||||||
|
description: 'Professional tier with curated feeds and hourly alerts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
name: 'Tycoon',
|
||||||
|
price: '29',
|
||||||
|
priceCurrency: 'USD',
|
||||||
|
priceValidUntil: '2025-12-31',
|
||||||
|
description: 'Enterprise tier with real-time alerts and unlimited portfolio',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
aggregateRating: {
|
||||||
|
'@type': 'AggregateRating',
|
||||||
|
ratingValue: '4.9',
|
||||||
|
ratingCount: '127',
|
||||||
|
bestRating: '5',
|
||||||
|
worstRating: '1',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function generateTLDPageSchema(tld: string, data: {
|
||||||
* Add UTM parameters for tracking
|
registrationPrice?: number
|
||||||
*/
|
renewalPrice?: number
|
||||||
export function addUTMParams(url: string, params: {
|
type?: string
|
||||||
source?: string
|
registrarCount?: number
|
||||||
medium?: string
|
}) {
|
||||||
campaign?: string
|
return {
|
||||||
content?: string
|
'@context': 'https://schema.org',
|
||||||
}): string {
|
'@type': 'Product',
|
||||||
const urlObj = new URL(url)
|
name: `.${tld} Domain Extension`,
|
||||||
if (params.source) urlObj.searchParams.set('utm_source', params.source)
|
description: `Current pricing, trends, and registrar comparison for .${tld} domains. Registration from $${data.registrationPrice || 'N/A'}, renewal $${data.renewalPrice || 'N/A'}.`,
|
||||||
if (params.medium) urlObj.searchParams.set('utm_medium', params.medium)
|
url: `${SITE_URL}/tld/${tld}`,
|
||||||
if (params.campaign) urlObj.searchParams.set('utm_campaign', params.campaign)
|
category: data.type || 'Domain Extension',
|
||||||
if (params.content) urlObj.searchParams.set('utm_content', params.content)
|
offers: {
|
||||||
return urlObj.toString()
|
'@type': 'AggregateOffer',
|
||||||
|
lowPrice: data.registrationPrice || 0,
|
||||||
|
highPrice: (data.renewalPrice || 0) * 10,
|
||||||
|
priceCurrency: 'USD',
|
||||||
|
offerCount: data.registrarCount || 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate breadcrumb JSON-LD
|
|
||||||
*/
|
|
||||||
export function generateBreadcrumbSchema(items: Array<{ name: string; url: string }>) {
|
export function generateBreadcrumbSchema(items: Array<{ name: string; url: string }>) {
|
||||||
return {
|
return {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
@ -205,40 +150,57 @@ export function generateBreadcrumbSchema(items: Array<{ name: string; url: strin
|
|||||||
'@type': 'ListItem',
|
'@type': 'ListItem',
|
||||||
position: index + 1,
|
position: index + 1,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
item: `${siteUrl}${item.url}`,
|
item: item.url,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function generateFAQSchema(faqs: Array<{ question: string; answer: string }>) {
|
||||||
* Performance: Critical CSS extraction helper
|
return {
|
||||||
*/
|
'@context': 'https://schema.org',
|
||||||
export function extractCriticalCSS(html: string): string {
|
'@type': 'FAQPage',
|
||||||
// This would be implemented with a CSS extraction library in production
|
mainEntity: faqs.map(faq => ({
|
||||||
// For now, return empty string
|
'@type': 'Question',
|
||||||
return ''
|
name: faq.question,
|
||||||
|
acceptedAnswer: {
|
||||||
|
'@type': 'Answer',
|
||||||
|
text: faq.answer,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function generateWebPageSchema(page: {
|
||||||
* Lazy load images with IntersectionObserver
|
title: string
|
||||||
*/
|
description: string
|
||||||
export function setupLazyLoading() {
|
url: string
|
||||||
if (typeof window === 'undefined') return
|
datePublished?: string
|
||||||
|
dateModified?: string
|
||||||
const imageObserver = new IntersectionObserver((entries, observer) => {
|
}) {
|
||||||
entries.forEach((entry) => {
|
return {
|
||||||
if (entry.isIntersecting) {
|
'@context': 'https://schema.org',
|
||||||
const img = entry.target as HTMLImageElement
|
'@type': 'WebPage',
|
||||||
if (img.dataset.src) {
|
name: page.title,
|
||||||
img.src = img.dataset.src
|
description: page.description,
|
||||||
img.removeAttribute('data-src')
|
url: page.url,
|
||||||
observer.unobserve(img)
|
isPartOf: {
|
||||||
}
|
'@type': 'WebSite',
|
||||||
}
|
name: SITE_NAME,
|
||||||
})
|
url: SITE_URL,
|
||||||
})
|
},
|
||||||
|
datePublished: page.datePublished || '2024-01-01',
|
||||||
document.querySelectorAll('img[data-src]').forEach((img) => {
|
dateModified: page.dateModified || new Date().toISOString().split('T')[0],
|
||||||
imageObserver.observe(img)
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Popular TLDs for static generation
|
||||||
|
export const POPULAR_TLDS = [
|
||||||
|
'com', 'net', 'org', 'io', 'ai', 'co', 'app', 'dev', 'xyz', 'online',
|
||||||
|
'store', 'shop', 'tech', 'site', 'club', 'info', 'biz', 'me', 'tv', 'cc',
|
||||||
|
'ch', 'de', 'uk', 'fr', 'es', 'it', 'nl', 'at', 'be', 'pl',
|
||||||
|
'au', 'ca', 'us', 'mx', 'br', 'ar', 'jp', 'cn', 'kr', 'in',
|
||||||
|
'cloud', 'digital', 'agency', 'studio', 'media', 'design', 'solutions', 'consulting',
|
||||||
|
'finance', 'money', 'bank', 'insurance', 'invest', 'capital', 'fund', 'trading',
|
||||||
|
'health', 'fitness', 'beauty', 'fashion', 'style', 'luxury', 'vip', 'pro',
|
||||||
|
'crypto', 'nft', 'web3', 'blockchain', 'bitcoin', 'defi', 'dao', 'token',
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user