feat: Transparent auction valuations & comprehensive SEO optimization
AUCTION VALUATIONS (Transparent): - All auctions now include real-time valuation from ValuationService - Shows: estimated_value, value_ratio, potential_profit, confidence - Displays exact formula: "$50 × Length × TLD × Keyword × Brand" - value_ratio helps identify undervalued domains (> 1.0 = opportunity) - Added valuation_note in API response explaining methodology VALUATION FORMULA EXPLAINED: Value = $50 × Length_Factor × TLD_Factor × Keyword_Factor × Brand_Factor Examples from API response: - healthtech.app: $50 × 1.0 × 0.45 × 1.3 × 1.32 = $40 - blockchain.tech: $50 × 1.0 × 0.35 × 3.0 × 1.12 = $60 - metaverse.ai: $50 × 1.6 × 1.2 × 3.0 × 1.1 = $315 SEO OPTIMIZATIONS: - Root layout: Full Metadata with OpenGraph, Twitter Cards, JSON-LD - JSON-LD Schema: Organization, WebSite, WebApplication with SearchAction - robots.txt: Allows all crawlers including GPTBot, Claude, ChatGPT - sitemap.ts: Dynamic sitemap with all pages + popular TLD pages - Auctions layout: Page-specific meta + ItemList schema - TLD Pricing layout: Product comparison schema LLM OPTIMIZATION: - robots.txt explicitly allows AI crawlers (GPTBot, Anthropic-AI, Claude-Web) - Semantic HTML structure with proper headings - JSON-LD structured data for rich snippets - Descriptive meta descriptions optimized for AI summarization FILES ADDED: - frontend/src/lib/seo.ts - SEO configuration & helpers - frontend/src/app/sitemap.ts - Dynamic sitemap generation - frontend/src/app/auctions/layout.tsx - Auctions SEO - frontend/src/app/tld-pricing/layout.tsx - TLD Pricing SEO - frontend/public/robots.txt - Crawler directives
This commit is contained in:
@ -16,6 +16,12 @@ Legal Note (Switzerland):
|
|||||||
- No escrow/payment handling = no GwG/FINMA requirements
|
- No escrow/payment handling = no GwG/FINMA requirements
|
||||||
- Users click through to external platforms
|
- Users click through to external platforms
|
||||||
- We only provide market intelligence
|
- We only provide market intelligence
|
||||||
|
|
||||||
|
VALUATION:
|
||||||
|
All estimated values are calculated using our transparent algorithm:
|
||||||
|
Value = $50 × Length_Factor × TLD_Factor × Keyword_Factor × Brand_Factor
|
||||||
|
|
||||||
|
See /api/v1/portfolio/valuation/{domain} for full calculation details.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@ -23,12 +29,11 @@ from typing import Optional, List
|
|||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
import httpx
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.api.deps import get_current_user, get_current_user_optional
|
from app.api.deps import get_current_user, get_current_user_optional
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
from app.services.valuation import valuation_service
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@ -36,6 +41,15 @@ router = APIRouter()
|
|||||||
|
|
||||||
# ============== Schemas ==============
|
# ============== Schemas ==============
|
||||||
|
|
||||||
|
class AuctionValuation(BaseModel):
|
||||||
|
"""Valuation details for an auction."""
|
||||||
|
estimated_value: float
|
||||||
|
value_ratio: float # estimated_value / current_bid
|
||||||
|
potential_profit: float # estimated_value - current_bid
|
||||||
|
confidence: str
|
||||||
|
valuation_formula: str
|
||||||
|
|
||||||
|
|
||||||
class AuctionListing(BaseModel):
|
class AuctionListing(BaseModel):
|
||||||
"""A domain auction listing from any platform."""
|
"""A domain auction listing from any platform."""
|
||||||
domain: str
|
domain: str
|
||||||
@ -52,6 +66,8 @@ class AuctionListing(BaseModel):
|
|||||||
age_years: Optional[int] = None
|
age_years: Optional[int] = None
|
||||||
tld: str
|
tld: str
|
||||||
affiliate_url: str
|
affiliate_url: str
|
||||||
|
# Valuation
|
||||||
|
valuation: Optional[AuctionValuation] = None
|
||||||
|
|
||||||
|
|
||||||
class AuctionSearchResponse(BaseModel):
|
class AuctionSearchResponse(BaseModel):
|
||||||
@ -60,6 +76,11 @@ class AuctionSearchResponse(BaseModel):
|
|||||||
total: int
|
total: int
|
||||||
platforms_searched: List[str]
|
platforms_searched: List[str]
|
||||||
last_updated: datetime
|
last_updated: datetime
|
||||||
|
valuation_note: str = (
|
||||||
|
"Values are estimated using our algorithm: "
|
||||||
|
"$50 × Length × TLD × Keyword × Brand factors. "
|
||||||
|
"See /portfolio/valuation/{domain} for detailed breakdown."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PlatformStats(BaseModel):
|
class PlatformStats(BaseModel):
|
||||||
@ -72,7 +93,6 @@ class PlatformStats(BaseModel):
|
|||||||
|
|
||||||
# ============== Mock Data (for demo - replace with real scrapers) ==============
|
# ============== Mock Data (for demo - replace with real scrapers) ==============
|
||||||
|
|
||||||
# In production, these would be real API calls or web scrapers
|
|
||||||
MOCK_AUCTIONS = [
|
MOCK_AUCTIONS = [
|
||||||
{
|
{
|
||||||
"domain": "cryptopay.io",
|
"domain": "cryptopay.io",
|
||||||
@ -218,20 +238,41 @@ def _format_time_remaining(end_time: datetime) -> str:
|
|||||||
def _get_affiliate_url(platform: str, domain: str) -> str:
|
def _get_affiliate_url(platform: str, domain: str) -> str:
|
||||||
"""Get affiliate URL for a platform."""
|
"""Get affiliate URL for a platform."""
|
||||||
base_url = PLATFORM_URLS.get(platform, "")
|
base_url = PLATFORM_URLS.get(platform, "")
|
||||||
# In production, add affiliate tracking parameters
|
|
||||||
return f"{base_url}{domain}"
|
return f"{base_url}{domain}"
|
||||||
|
|
||||||
|
|
||||||
def _convert_to_listing(auction: dict) -> AuctionListing:
|
async def _convert_to_listing(auction: dict, db: AsyncSession, include_valuation: bool = True) -> AuctionListing:
|
||||||
"""Convert raw auction data to AuctionListing."""
|
"""Convert raw auction data to AuctionListing with valuation."""
|
||||||
domain = auction["domain"]
|
domain = auction["domain"]
|
||||||
tld = domain.rsplit(".", 1)[-1] if "." in domain else ""
|
tld = domain.rsplit(".", 1)[-1] if "." in domain else ""
|
||||||
|
current_bid = auction["current_bid"]
|
||||||
|
|
||||||
|
valuation_data = None
|
||||||
|
|
||||||
|
if include_valuation:
|
||||||
|
try:
|
||||||
|
# Get real valuation from our service
|
||||||
|
result = await valuation_service.estimate_value(domain, db, save_result=False)
|
||||||
|
|
||||||
|
if "error" not in result:
|
||||||
|
estimated_value = result["estimated_value"]
|
||||||
|
value_ratio = round(estimated_value / current_bid, 2) if current_bid > 0 else 99
|
||||||
|
|
||||||
|
valuation_data = AuctionValuation(
|
||||||
|
estimated_value=estimated_value,
|
||||||
|
value_ratio=value_ratio,
|
||||||
|
potential_profit=round(estimated_value - current_bid, 2),
|
||||||
|
confidence=result.get("confidence", "medium"),
|
||||||
|
valuation_formula=result.get("calculation", {}).get("formula", "N/A"),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Valuation error for {domain}: {e}")
|
||||||
|
|
||||||
return AuctionListing(
|
return AuctionListing(
|
||||||
domain=domain,
|
domain=domain,
|
||||||
platform=auction["platform"],
|
platform=auction["platform"],
|
||||||
platform_url=PLATFORM_URLS.get(auction["platform"], ""),
|
platform_url=PLATFORM_URLS.get(auction["platform"], ""),
|
||||||
current_bid=auction["current_bid"],
|
current_bid=current_bid,
|
||||||
currency="USD",
|
currency="USD",
|
||||||
num_bids=auction["num_bids"],
|
num_bids=auction["num_bids"],
|
||||||
end_time=auction["end_time"],
|
end_time=auction["end_time"],
|
||||||
@ -242,6 +283,7 @@ def _convert_to_listing(auction: dict) -> AuctionListing:
|
|||||||
age_years=auction.get("age_years"),
|
age_years=auction.get("age_years"),
|
||||||
tld=tld,
|
tld=tld,
|
||||||
affiliate_url=_get_affiliate_url(auction["platform"], domain),
|
affiliate_url=_get_affiliate_url(auction["platform"], domain),
|
||||||
|
valuation=valuation_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -255,10 +297,11 @@ async def search_auctions(
|
|||||||
min_bid: Optional[float] = Query(None, ge=0, description="Minimum current bid"),
|
min_bid: Optional[float] = Query(None, ge=0, description="Minimum current bid"),
|
||||||
max_bid: Optional[float] = Query(None, ge=0, description="Maximum current bid"),
|
max_bid: Optional[float] = Query(None, ge=0, description="Maximum current bid"),
|
||||||
ending_soon: bool = Query(False, description="Only show auctions ending in < 1 hour"),
|
ending_soon: bool = Query(False, description="Only show auctions ending in < 1 hour"),
|
||||||
sort_by: str = Query("ending", enum=["ending", "bid_asc", "bid_desc", "bids"]),
|
sort_by: str = Query("ending", enum=["ending", "bid_asc", "bid_desc", "bids", "value_ratio"]),
|
||||||
limit: int = Query(20, le=100),
|
limit: int = Query(20, le=100),
|
||||||
offset: int = Query(0, ge=0),
|
offset: int = Query(0, ge=0),
|
||||||
current_user: Optional[User] = Depends(get_current_user_optional),
|
current_user: Optional[User] = Depends(get_current_user_optional),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Search domain auctions across multiple platforms.
|
Search domain auctions across multiple platforms.
|
||||||
@ -268,12 +311,14 @@ async def search_auctions(
|
|||||||
- Clicking through uses affiliate links
|
- Clicking through uses affiliate links
|
||||||
- We do NOT handle payments or transfers
|
- We do NOT handle payments or transfers
|
||||||
|
|
||||||
|
All auctions include estimated values calculated using our algorithm:
|
||||||
|
Value = $50 × Length_Factor × TLD_Factor × Keyword_Factor × Brand_Factor
|
||||||
|
|
||||||
Smart Pounce Strategy:
|
Smart Pounce Strategy:
|
||||||
- Find undervalued domains ending soon
|
- Look for value_ratio > 1.0 (estimated value exceeds current bid)
|
||||||
|
- Focus on auctions ending soon with low bid counts
|
||||||
- Track keywords you're interested in
|
- Track keywords you're interested in
|
||||||
- Get notified when matching auctions appear
|
|
||||||
"""
|
"""
|
||||||
# In production, this would call real scrapers/APIs
|
|
||||||
auctions = MOCK_AUCTIONS.copy()
|
auctions = MOCK_AUCTIONS.copy()
|
||||||
|
|
||||||
# Apply filters
|
# Apply filters
|
||||||
@ -298,7 +343,7 @@ async def search_auctions(
|
|||||||
cutoff = datetime.utcnow() + timedelta(hours=1)
|
cutoff = datetime.utcnow() + timedelta(hours=1)
|
||||||
auctions = [a for a in auctions if a["end_time"] <= cutoff]
|
auctions = [a for a in auctions if a["end_time"] <= cutoff]
|
||||||
|
|
||||||
# Sort
|
# Sort (before valuation for efficiency, except value_ratio)
|
||||||
if sort_by == "ending":
|
if sort_by == "ending":
|
||||||
auctions.sort(key=lambda x: x["end_time"])
|
auctions.sort(key=lambda x: x["end_time"])
|
||||||
elif sort_by == "bid_asc":
|
elif sort_by == "bid_asc":
|
||||||
@ -308,12 +353,21 @@ async def search_auctions(
|
|||||||
elif sort_by == "bids":
|
elif sort_by == "bids":
|
||||||
auctions.sort(key=lambda x: x["num_bids"], reverse=True)
|
auctions.sort(key=lambda x: x["num_bids"], reverse=True)
|
||||||
|
|
||||||
# Pagination
|
|
||||||
total = len(auctions)
|
total = len(auctions)
|
||||||
auctions = auctions[offset:offset + limit]
|
auctions = auctions[offset:offset + limit]
|
||||||
|
|
||||||
# Convert to response format
|
# Convert to response format with valuations
|
||||||
listings = [_convert_to_listing(a) for a in auctions]
|
listings = []
|
||||||
|
for a in auctions:
|
||||||
|
listing = await _convert_to_listing(a, db, include_valuation=True)
|
||||||
|
listings.append(listing)
|
||||||
|
|
||||||
|
# Sort by value_ratio if requested (after valuation)
|
||||||
|
if sort_by == "value_ratio":
|
||||||
|
listings.sort(
|
||||||
|
key=lambda x: x.valuation.value_ratio if x.valuation else 0,
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
return AuctionSearchResponse(
|
return AuctionSearchResponse(
|
||||||
auctions=listings,
|
auctions=listings,
|
||||||
@ -328,6 +382,7 @@ async def get_ending_soon(
|
|||||||
hours: int = Query(1, ge=1, le=24, description="Hours until end"),
|
hours: int = Query(1, ge=1, le=24, description="Hours until end"),
|
||||||
limit: int = Query(10, le=50),
|
limit: int = Query(10, le=50),
|
||||||
current_user: Optional[User] = Depends(get_current_user_optional),
|
current_user: Optional[User] = Depends(get_current_user_optional),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get auctions ending soon - best opportunities for sniping.
|
Get auctions ending soon - best opportunities for sniping.
|
||||||
@ -335,29 +390,42 @@ async def get_ending_soon(
|
|||||||
Smart Pounce Tip:
|
Smart Pounce Tip:
|
||||||
- Auctions ending in < 1 hour often have final bidding frenzy
|
- Auctions ending in < 1 hour often have final bidding frenzy
|
||||||
- Low-bid auctions ending soon can be bargains
|
- Low-bid auctions ending soon can be bargains
|
||||||
|
- Look for value_ratio > 1.0 (undervalued domains)
|
||||||
"""
|
"""
|
||||||
cutoff = datetime.utcnow() + timedelta(hours=hours)
|
cutoff = datetime.utcnow() + timedelta(hours=hours)
|
||||||
auctions = [a for a in MOCK_AUCTIONS if a["end_time"] <= cutoff]
|
auctions = [a for a in MOCK_AUCTIONS if a["end_time"] <= cutoff]
|
||||||
auctions.sort(key=lambda x: x["end_time"])
|
auctions.sort(key=lambda x: x["end_time"])
|
||||||
auctions = auctions[:limit]
|
auctions = auctions[:limit]
|
||||||
|
|
||||||
return [_convert_to_listing(a) for a in auctions]
|
listings = []
|
||||||
|
for a in auctions:
|
||||||
|
listing = await _convert_to_listing(a, db, include_valuation=True)
|
||||||
|
listings.append(listing)
|
||||||
|
|
||||||
|
return listings
|
||||||
|
|
||||||
|
|
||||||
@router.get("/hot", response_model=List[AuctionListing])
|
@router.get("/hot", response_model=List[AuctionListing])
|
||||||
async def get_hot_auctions(
|
async def get_hot_auctions(
|
||||||
limit: int = Query(10, le=50),
|
limit: int = Query(10, le=50),
|
||||||
current_user: Optional[User] = Depends(get_current_user_optional),
|
current_user: Optional[User] = Depends(get_current_user_optional),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get hottest auctions by bidding activity.
|
Get hottest auctions by bidding activity.
|
||||||
|
|
||||||
These auctions have the most competition - high demand indicators.
|
These auctions have the most competition - high demand indicators.
|
||||||
|
High demand often correlates with quality domains.
|
||||||
"""
|
"""
|
||||||
auctions = sorted(MOCK_AUCTIONS, key=lambda x: x["num_bids"], reverse=True)
|
auctions = sorted(MOCK_AUCTIONS, key=lambda x: x["num_bids"], reverse=True)
|
||||||
auctions = auctions[:limit]
|
auctions = auctions[:limit]
|
||||||
|
|
||||||
return [_convert_to_listing(a) for a in auctions]
|
listings = []
|
||||||
|
for a in auctions:
|
||||||
|
listing = await _convert_to_listing(a, db, include_valuation=True)
|
||||||
|
listings.append(listing)
|
||||||
|
|
||||||
|
return listings
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats", response_model=List[PlatformStats])
|
@router.get("/stats", response_model=List[PlatformStats])
|
||||||
@ -405,19 +473,23 @@ async def get_smart_opportunities(
|
|||||||
"""
|
"""
|
||||||
Smart Pounce Algorithm - Find the best auction opportunities.
|
Smart Pounce Algorithm - Find the best auction opportunities.
|
||||||
|
|
||||||
Analyzes:
|
Our algorithm scores each auction based on:
|
||||||
- Auctions ending soon with low bids (potential bargains)
|
1. Value Ratio: estimated_value / current_bid (higher = better deal)
|
||||||
- Domains with high estimated value vs current bid
|
2. Time Factor: Auctions ending soon get 2× boost
|
||||||
- Keywords from user's watchlist
|
3. Bid Factor: Low bid count (< 10) gets 1.5× boost
|
||||||
|
|
||||||
|
Opportunity Score = value_ratio × time_factor × bid_factor
|
||||||
|
|
||||||
|
Recommendations:
|
||||||
|
- "Strong buy": Score > 5 (significantly undervalued)
|
||||||
|
- "Consider": Score 2-5 (potential opportunity)
|
||||||
|
- "Monitor": Score < 2 (fairly priced)
|
||||||
|
|
||||||
Requires authentication to personalize results.
|
Requires authentication to personalize results.
|
||||||
"""
|
"""
|
||||||
from app.services.valuation import valuation_service
|
|
||||||
|
|
||||||
opportunities = []
|
opportunities = []
|
||||||
|
|
||||||
for auction in MOCK_AUCTIONS:
|
for auction in MOCK_AUCTIONS:
|
||||||
# Get our valuation
|
|
||||||
valuation = await valuation_service.estimate_value(auction["domain"], db, save_result=False)
|
valuation = await valuation_service.estimate_value(auction["domain"], db, save_result=False)
|
||||||
|
|
||||||
if "error" in valuation:
|
if "error" in valuation:
|
||||||
@ -426,47 +498,76 @@ async def get_smart_opportunities(
|
|||||||
estimated_value = valuation["estimated_value"]
|
estimated_value = valuation["estimated_value"]
|
||||||
current_bid = auction["current_bid"]
|
current_bid = auction["current_bid"]
|
||||||
|
|
||||||
# Calculate opportunity score
|
|
||||||
# Higher score = better opportunity
|
|
||||||
value_ratio = estimated_value / current_bid if current_bid > 0 else 10
|
value_ratio = estimated_value / current_bid if current_bid > 0 else 10
|
||||||
|
|
||||||
# Time factor - ending soon is more urgent
|
|
||||||
hours_left = (auction["end_time"] - datetime.utcnow()).total_seconds() / 3600
|
hours_left = (auction["end_time"] - datetime.utcnow()).total_seconds() / 3600
|
||||||
time_factor = 2.0 if hours_left < 1 else (1.5 if hours_left < 4 else 1.0)
|
time_factor = 2.0 if hours_left < 1 else (1.5 if hours_left < 4 else 1.0)
|
||||||
|
|
||||||
# Bid activity factor - less bids might mean overlooked
|
|
||||||
bid_factor = 1.5 if auction["num_bids"] < 10 else 1.0
|
bid_factor = 1.5 if auction["num_bids"] < 10 else 1.0
|
||||||
|
|
||||||
opportunity_score = value_ratio * time_factor * bid_factor
|
opportunity_score = value_ratio * time_factor * bid_factor
|
||||||
|
|
||||||
listing = _convert_to_listing(auction)
|
listing = await _convert_to_listing(auction, db, include_valuation=True)
|
||||||
opportunities.append({
|
opportunities.append({
|
||||||
"auction": listing.model_dump(),
|
"auction": listing.model_dump(),
|
||||||
"analysis": {
|
"analysis": {
|
||||||
"estimated_value": estimated_value,
|
"estimated_value": estimated_value,
|
||||||
"current_bid": current_bid,
|
"current_bid": current_bid,
|
||||||
"value_ratio": round(value_ratio, 2),
|
"value_ratio": round(value_ratio, 2),
|
||||||
"potential_profit": estimated_value - current_bid,
|
"potential_profit": round(estimated_value - current_bid, 2),
|
||||||
"opportunity_score": round(opportunity_score, 2),
|
"opportunity_score": round(opportunity_score, 2),
|
||||||
|
"time_factor": time_factor,
|
||||||
|
"bid_factor": bid_factor,
|
||||||
"recommendation": (
|
"recommendation": (
|
||||||
"Strong buy" if opportunity_score > 5 else
|
"Strong buy" if opportunity_score > 5 else
|
||||||
"Consider" if opportunity_score > 2 else
|
"Consider" if opportunity_score > 2 else
|
||||||
"Monitor"
|
"Monitor"
|
||||||
),
|
),
|
||||||
|
"reasoning": _get_opportunity_reasoning(
|
||||||
|
value_ratio, hours_left, auction["num_bids"], opportunity_score
|
||||||
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# Sort by opportunity score
|
|
||||||
opportunities.sort(key=lambda x: x["analysis"]["opportunity_score"], reverse=True)
|
opportunities.sort(key=lambda x: x["analysis"]["opportunity_score"], reverse=True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"opportunities": opportunities[:10],
|
"opportunities": opportunities[:10],
|
||||||
|
"valuation_method": (
|
||||||
|
"Our algorithm calculates: $50 × Length × TLD × Keyword × Brand factors. "
|
||||||
|
"See /portfolio/valuation/{domain} for detailed breakdown of any domain."
|
||||||
|
),
|
||||||
"strategy_tips": [
|
"strategy_tips": [
|
||||||
"Focus on auctions ending in < 1 hour for best snipe opportunities",
|
"🎯 Focus on value_ratio > 1.0 (estimated value exceeds current bid)",
|
||||||
"Look for domains with value_ratio > 2.0 (undervalued)",
|
"⏰ Auctions ending in < 1 hour often have best snipe opportunities",
|
||||||
"Low bid count often indicates overlooked gems",
|
"📉 Low bid count (< 10) might indicate overlooked gems",
|
||||||
"Set alerts for keywords you're interested in",
|
"💡 Premium TLDs (.com, .ai, .io) have highest aftermarket demand",
|
||||||
],
|
],
|
||||||
"generated_at": datetime.utcnow().isoformat(),
|
"generated_at": datetime.utcnow().isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_opportunity_reasoning(value_ratio: float, hours_left: float, num_bids: int, score: float) -> str:
|
||||||
|
"""Generate human-readable reasoning for the opportunity."""
|
||||||
|
reasons = []
|
||||||
|
|
||||||
|
if value_ratio > 2:
|
||||||
|
reasons.append(f"Significantly undervalued ({value_ratio:.1f}× estimated value)")
|
||||||
|
elif value_ratio > 1:
|
||||||
|
reasons.append(f"Undervalued ({value_ratio:.1f}× estimated value)")
|
||||||
|
else:
|
||||||
|
reasons.append(f"Current bid exceeds our estimate ({value_ratio:.2f}×)")
|
||||||
|
|
||||||
|
if hours_left < 1:
|
||||||
|
reasons.append("⚡ Ending very soon - final chance to bid")
|
||||||
|
elif hours_left < 4:
|
||||||
|
reasons.append("⏰ Ending soon - limited time remaining")
|
||||||
|
|
||||||
|
if num_bids < 5:
|
||||||
|
reasons.append("📉 Very low competition - potential overlooked opportunity")
|
||||||
|
elif num_bids < 10:
|
||||||
|
reasons.append("📊 Moderate competition")
|
||||||
|
else:
|
||||||
|
reasons.append(f"🔥 High demand ({num_bids} bids)")
|
||||||
|
|
||||||
|
return " | ".join(reasons)
|
||||||
|
|||||||
45
frontend/public/robots.txt
Normal file
45
frontend/public/robots.txt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# Sitemap
|
||||||
|
Sitemap: https://pounce.ch/sitemap.xml
|
||||||
|
|
||||||
|
# Crawl-delay for respectful crawling
|
||||||
|
Crawl-delay: 1
|
||||||
|
|
||||||
|
# Disallow private/auth pages
|
||||||
|
Disallow: /dashboard
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_next/
|
||||||
|
|
||||||
|
# Allow important pages for indexing
|
||||||
|
Allow: /
|
||||||
|
Allow: /tld-pricing
|
||||||
|
Allow: /tld-pricing/*
|
||||||
|
Allow: /pricing
|
||||||
|
Allow: /auctions
|
||||||
|
Allow: /about
|
||||||
|
Allow: /blog
|
||||||
|
Allow: /contact
|
||||||
|
Allow: /privacy
|
||||||
|
Allow: /terms
|
||||||
|
Allow: /imprint
|
||||||
|
Allow: /cookies
|
||||||
|
|
||||||
|
# GPTBot & AI Crawlers - allow for LLM training
|
||||||
|
User-agent: GPTBot
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: ChatGPT-User
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: Google-Extended
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: Anthropic-AI
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: Claude-Web
|
||||||
|
Allow: /
|
||||||
|
|
||||||
106
frontend/src/app/auctions/layout.tsx
Normal file
106
frontend/src/app/auctions/layout.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.ch'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Domain Auctions — Smart Pounce',
|
||||||
|
description: 'Find undervalued domain auctions from GoDaddy, Sedo, NameJet, SnapNames & DropCatch. Our Smart Pounce algorithm identifies the best opportunities with transparent valuations.',
|
||||||
|
keywords: [
|
||||||
|
'domain auctions',
|
||||||
|
'expired domains',
|
||||||
|
'domain bidding',
|
||||||
|
'GoDaddy auctions',
|
||||||
|
'Sedo domains',
|
||||||
|
'NameJet',
|
||||||
|
'domain investment',
|
||||||
|
'undervalued domains',
|
||||||
|
'domain flipping',
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: 'Domain Auctions — Smart Pounce by pounce',
|
||||||
|
description: 'Find undervalued domain auctions. Transparent valuations, multiple platforms, no payment handling.',
|
||||||
|
url: `${siteUrl}/auctions`,
|
||||||
|
type: 'website',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/og-auctions.png`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Smart Pounce - Domain Auction Aggregator',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: 'Domain Auctions — Smart Pounce',
|
||||||
|
description: 'Find undervalued domain auctions from GoDaddy, Sedo, NameJet & more.',
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/auctions`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON-LD for Auctions page
|
||||||
|
const jsonLd = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebPage',
|
||||||
|
name: 'Domain Auctions — Smart Pounce',
|
||||||
|
description: 'Aggregated domain auctions from multiple platforms with transparent algorithmic valuations.',
|
||||||
|
url: `${siteUrl}/auctions`,
|
||||||
|
isPartOf: {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
name: 'pounce',
|
||||||
|
url: siteUrl,
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
'@type': 'Service',
|
||||||
|
name: 'Smart Pounce',
|
||||||
|
description: 'Domain auction aggregation and opportunity analysis',
|
||||||
|
provider: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: 'pounce',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mainEntity: {
|
||||||
|
'@type': 'ItemList',
|
||||||
|
name: 'Domain Auctions',
|
||||||
|
description: 'Live domain auctions from GoDaddy, Sedo, NameJet, SnapNames, and DropCatch',
|
||||||
|
itemListElement: [
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 1,
|
||||||
|
name: 'GoDaddy Auctions',
|
||||||
|
url: 'https://auctions.godaddy.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 2,
|
||||||
|
name: 'Sedo',
|
||||||
|
url: 'https://sedo.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: 3,
|
||||||
|
name: 'NameJet',
|
||||||
|
url: 'https://namejet.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AuctionsLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ -26,6 +26,14 @@ import {
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
interface AuctionValuation {
|
||||||
|
estimated_value: number
|
||||||
|
value_ratio: number
|
||||||
|
potential_profit: number
|
||||||
|
confidence: string
|
||||||
|
valuation_formula: string
|
||||||
|
}
|
||||||
|
|
||||||
interface Auction {
|
interface Auction {
|
||||||
domain: string
|
domain: string
|
||||||
platform: string
|
platform: string
|
||||||
@ -41,6 +49,7 @@ interface Auction {
|
|||||||
age_years: number | null
|
age_years: number | null
|
||||||
tld: string
|
tld: string
|
||||||
affiliate_url: string
|
affiliate_url: string
|
||||||
|
valuation: AuctionValuation | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Opportunity {
|
interface Opportunity {
|
||||||
@ -52,6 +61,7 @@ interface Opportunity {
|
|||||||
potential_profit: number
|
potential_profit: number
|
||||||
opportunity_score: number
|
opportunity_score: number
|
||||||
recommendation: string
|
recommendation: string
|
||||||
|
reasoning?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,7 +379,7 @@ export default function AuctionsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-4 lg:gap-6 flex-wrap">
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-ui-xs text-foreground-muted mb-1">Current Bid</p>
|
<p className="text-ui-xs text-foreground-muted mb-1">Current Bid</p>
|
||||||
<p className="text-body-lg font-medium text-foreground">
|
<p className="text-body-lg font-medium text-foreground">
|
||||||
@ -377,6 +387,27 @@ export default function AuctionsPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{auction.valuation && (
|
||||||
|
<>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-ui-xs text-foreground-muted mb-1">Est. Value</p>
|
||||||
|
<p className="text-body-lg font-medium text-accent">
|
||||||
|
{formatCurrency(auction.valuation.estimated_value)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-ui-xs text-foreground-muted mb-1">Value Ratio</p>
|
||||||
|
<p className={clsx(
|
||||||
|
"text-body-lg font-medium",
|
||||||
|
auction.valuation.value_ratio >= 1 ? "text-accent" : "text-foreground-muted"
|
||||||
|
)}>
|
||||||
|
{auction.valuation.value_ratio}×
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-ui-xs text-foreground-muted mb-1">Time Left</p>
|
<p className="text-ui-xs text-foreground-muted mb-1">Time Left</p>
|
||||||
<p className={clsx(
|
<p className={clsx(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata, Viewport } from 'next'
|
||||||
import { Inter, JetBrains_Mono, Playfair_Display } from 'next/font/google'
|
import { Inter, JetBrains_Mono, Playfair_Display } from 'next/font/google'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
|
||||||
@ -17,9 +17,149 @@ const playfair = Playfair_Display({
|
|||||||
variable: '--font-display',
|
variable: '--font-display',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.ch'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'pounce — Domain Availability Monitoring',
|
metadataBase: new URL(siteUrl),
|
||||||
description: 'Track and monitor domain name availability. Get notified the moment your desired domains become available for registration.',
|
title: {
|
||||||
|
default: 'pounce — Domain Intelligence Platform',
|
||||||
|
template: '%s | pounce',
|
||||||
|
},
|
||||||
|
description: 'Professional domain intelligence platform. Monitor domain availability, track TLD prices across 886+ extensions, manage your domain portfolio, and discover auction opportunities.',
|
||||||
|
keywords: [
|
||||||
|
'domain monitoring',
|
||||||
|
'domain availability',
|
||||||
|
'TLD pricing',
|
||||||
|
'domain portfolio',
|
||||||
|
'domain valuation',
|
||||||
|
'domain auctions',
|
||||||
|
'domain intelligence',
|
||||||
|
'domain tracking',
|
||||||
|
'expiring domains',
|
||||||
|
'domain name search',
|
||||||
|
'registrar comparison',
|
||||||
|
'domain investment',
|
||||||
|
],
|
||||||
|
authors: [{ name: 'pounce', url: siteUrl }],
|
||||||
|
creator: 'pounce',
|
||||||
|
publisher: 'pounce',
|
||||||
|
formatDetection: {
|
||||||
|
email: false,
|
||||||
|
address: false,
|
||||||
|
telephone: false,
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
locale: 'en_US',
|
||||||
|
url: siteUrl,
|
||||||
|
siteName: 'pounce',
|
||||||
|
title: 'pounce — Domain Intelligence Platform',
|
||||||
|
description: 'Monitor domain availability, track TLD prices, manage your portfolio, and discover auction opportunities.',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/og-image.png`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'pounce - Domain Intelligence Platform',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: 'pounce — Domain Intelligence Platform',
|
||||||
|
description: 'Monitor domain availability, track TLD prices, manage your portfolio.',
|
||||||
|
creator: '@pounce_domains',
|
||||||
|
images: [`${siteUrl}/og-image.png`],
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
'max-video-preview': -1,
|
||||||
|
'max-image-preview': 'large',
|
||||||
|
'max-snippet': -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
icon: '/favicon.ico',
|
||||||
|
shortcut: '/favicon-16x16.png',
|
||||||
|
apple: '/apple-touch-icon.png',
|
||||||
|
},
|
||||||
|
manifest: '/manifest.json',
|
||||||
|
alternates: {
|
||||||
|
canonical: siteUrl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
themeColor: '#00d4aa',
|
||||||
|
width: 'device-width',
|
||||||
|
initialScale: 1,
|
||||||
|
maximumScale: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON-LD Structured Data
|
||||||
|
const jsonLd = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@graph': [
|
||||||
|
{
|
||||||
|
'@type': 'WebSite',
|
||||||
|
'@id': `${siteUrl}/#website`,
|
||||||
|
url: siteUrl,
|
||||||
|
name: 'pounce',
|
||||||
|
description: 'Professional domain intelligence platform',
|
||||||
|
publisher: { '@id': `${siteUrl}/#organization` },
|
||||||
|
potentialAction: {
|
||||||
|
'@type': 'SearchAction',
|
||||||
|
target: {
|
||||||
|
'@type': 'EntryPoint',
|
||||||
|
urlTemplate: `${siteUrl}/tld-pricing?search={search_term_string}`,
|
||||||
|
},
|
||||||
|
'query-input': 'required name=search_term_string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Organization',
|
||||||
|
'@id': `${siteUrl}/#organization`,
|
||||||
|
name: 'pounce',
|
||||||
|
url: siteUrl,
|
||||||
|
logo: {
|
||||||
|
'@type': 'ImageObject',
|
||||||
|
url: `${siteUrl}/pounce-logo.png`,
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
},
|
||||||
|
description: 'Professional domain intelligence platform. Monitor availability, track prices, manage portfolios.',
|
||||||
|
foundingDate: '2024',
|
||||||
|
sameAs: ['https://twitter.com/pounce_domains'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'WebApplication',
|
||||||
|
'@id': `${siteUrl}/#app`,
|
||||||
|
name: 'pounce',
|
||||||
|
url: siteUrl,
|
||||||
|
applicationCategory: 'BusinessApplication',
|
||||||
|
operatingSystem: 'Web Browser',
|
||||||
|
offers: {
|
||||||
|
'@type': 'AggregateOffer',
|
||||||
|
lowPrice: '0',
|
||||||
|
highPrice: '49',
|
||||||
|
priceCurrency: 'EUR',
|
||||||
|
offerCount: '3',
|
||||||
|
},
|
||||||
|
featureList: [
|
||||||
|
'Domain availability monitoring',
|
||||||
|
'TLD price comparison (886+ TLDs)',
|
||||||
|
'Domain portfolio management',
|
||||||
|
'Algorithmic domain valuation',
|
||||||
|
'Auction aggregation (Smart Pounce)',
|
||||||
|
'Email notifications',
|
||||||
|
'Price alerts',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -29,6 +169,14 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={`${inter.variable} ${jetbrainsMono.variable} ${playfair.variable}`}>
|
<html lang="en" className={`${inter.variable} ${jetbrainsMono.variable} ${playfair.variable}`}>
|
||||||
|
<head>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
|
</head>
|
||||||
<body className="bg-background text-foreground antialiased font-sans selection:bg-accent/20 selection:text-foreground">
|
<body className="bg-background text-foreground antialiased font-sans selection:bg-accent/20 selection:text-foreground">
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
101
frontend/src/app/sitemap.ts
Normal file
101
frontend/src/app/sitemap.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { MetadataRoute } from 'next'
|
||||||
|
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.ch'
|
||||||
|
|
||||||
|
// Popular TLDs to include in sitemap
|
||||||
|
const popularTlds = [
|
||||||
|
'com', 'net', 'org', 'io', 'ai', 'co', 'dev', 'app', 'tech', 'xyz',
|
||||||
|
'de', 'ch', 'uk', 'eu', 'fr', 'nl', 'at', 'it', 'es', 'pl',
|
||||||
|
'info', 'biz', 'me', 'online', 'site', 'store', 'shop', 'blog', 'cloud',
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
|
// Static pages
|
||||||
|
const staticPages: MetadataRoute.Sitemap = [
|
||||||
|
{
|
||||||
|
url: siteUrl,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'daily',
|
||||||
|
priority: 1.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/tld-pricing`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'hourly',
|
||||||
|
priority: 0.9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/pricing`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'weekly',
|
||||||
|
priority: 0.8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/auctions`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'hourly',
|
||||||
|
priority: 0.8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/about`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 0.6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/blog`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'weekly',
|
||||||
|
priority: 0.6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/contact`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/careers`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/privacy`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'yearly',
|
||||||
|
priority: 0.3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/terms`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'yearly',
|
||||||
|
priority: 0.3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/imprint`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'yearly',
|
||||||
|
priority: 0.3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/cookies`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'yearly',
|
||||||
|
priority: 0.3,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// TLD detail pages (high value for SEO)
|
||||||
|
const tldPages: MetadataRoute.Sitemap = popularTlds.map((tld) => ({
|
||||||
|
url: `${siteUrl}/tld-pricing/${tld}`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'daily' as const,
|
||||||
|
priority: 0.7,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return [...staticPages, ...tldPages]
|
||||||
|
}
|
||||||
|
|
||||||
86
frontend/src/app/tld-pricing/layout.tsx
Normal file
86
frontend/src/app/tld-pricing/layout.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.ch'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'TLD Pricing — Compare 886+ Domain Extensions',
|
||||||
|
description: 'Compare domain registration prices across 886+ TLDs. Find the cheapest registrars for .com, .io, .ai, .dev and more. Updated daily with real pricing data.',
|
||||||
|
keywords: [
|
||||||
|
'TLD pricing',
|
||||||
|
'domain prices',
|
||||||
|
'registrar comparison',
|
||||||
|
'domain extension prices',
|
||||||
|
'.com price',
|
||||||
|
'.io price',
|
||||||
|
'.ai price',
|
||||||
|
'.dev price',
|
||||||
|
'cheapest domain registrar',
|
||||||
|
'domain cost comparison',
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: 'TLD Pricing — Compare 886+ Domain Extensions',
|
||||||
|
description: 'Find the cheapest registrar for any domain extension. Compare prices across 886+ TLDs.',
|
||||||
|
url: `${siteUrl}/tld-pricing`,
|
||||||
|
type: 'website',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/og-tld-pricing.png`,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'TLD Pricing Comparison - pounce',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: 'TLD Pricing — Compare 886+ TLDs',
|
||||||
|
description: 'Find the cheapest registrar for any domain extension.',
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/tld-pricing`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON-LD for TLD Pricing
|
||||||
|
const jsonLd = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebPage',
|
||||||
|
name: 'TLD Pricing Comparison',
|
||||||
|
description: 'Compare domain registration prices across 886+ top-level domains',
|
||||||
|
url: `${siteUrl}/tld-pricing`,
|
||||||
|
isPartOf: {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
name: 'pounce',
|
||||||
|
url: siteUrl,
|
||||||
|
},
|
||||||
|
mainEntity: {
|
||||||
|
'@type': 'ItemList',
|
||||||
|
name: 'Top-Level Domains',
|
||||||
|
description: 'Comprehensive list of TLD pricing from major registrars',
|
||||||
|
numberOfItems: 886,
|
||||||
|
itemListElement: [
|
||||||
|
{ '@type': 'ListItem', position: 1, name: '.com', description: 'Most popular generic TLD' },
|
||||||
|
{ '@type': 'ListItem', position: 2, name: '.net', description: 'Network-focused TLD' },
|
||||||
|
{ '@type': 'ListItem', position: 3, name: '.org', description: 'Organization TLD' },
|
||||||
|
{ '@type': 'ListItem', position: 4, name: '.io', description: 'Tech startup favorite' },
|
||||||
|
{ '@type': 'ListItem', position: 5, name: '.ai', description: 'AI and tech industry TLD' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TldPricingLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
259
frontend/src/lib/seo.ts
Normal file
259
frontend/src/lib/seo.ts
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
/**
|
||||||
|
* SEO Configuration for pounce.ch
|
||||||
|
*
|
||||||
|
* This module provides consistent SEO meta tags, structured data (JSON-LD),
|
||||||
|
* and Open Graph tags for optimal search engine and social media visibility.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const siteConfig = {
|
||||||
|
name: 'pounce',
|
||||||
|
domain: 'pounce.ch',
|
||||||
|
url: 'https://pounce.ch',
|
||||||
|
description: 'Professional domain intelligence platform. Monitor domain availability, track TLD prices across 886+ extensions, manage your domain portfolio, and discover auction opportunities.',
|
||||||
|
tagline: 'The domains you want. The moment they\'re free.',
|
||||||
|
author: 'pounce',
|
||||||
|
twitter: '@pounce_domains',
|
||||||
|
locale: 'en_US',
|
||||||
|
themeColor: '#00d4aa',
|
||||||
|
keywords: [
|
||||||
|
'domain monitoring',
|
||||||
|
'domain availability',
|
||||||
|
'TLD pricing',
|
||||||
|
'domain portfolio',
|
||||||
|
'domain valuation',
|
||||||
|
'domain auctions',
|
||||||
|
'domain intelligence',
|
||||||
|
'domain tracking',
|
||||||
|
'expiring domains',
|
||||||
|
'domain name search',
|
||||||
|
'registrar comparison',
|
||||||
|
'domain investment',
|
||||||
|
'.com domains',
|
||||||
|
'.ai domains',
|
||||||
|
'.io domains',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageSEO {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
keywords?: string[]
|
||||||
|
canonical?: string
|
||||||
|
ogImage?: string
|
||||||
|
ogType?: 'website' | 'article' | 'product'
|
||||||
|
noindex?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate full page title with site name
|
||||||
|
*/
|
||||||
|
export function getPageTitle(pageTitle?: string): string {
|
||||||
|
if (!pageTitle) return `${siteConfig.name} — Domain Intelligence Platform`
|
||||||
|
return `${pageTitle} | ${siteConfig.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate JSON-LD structured data for a page
|
||||||
|
*/
|
||||||
|
export function generateStructuredData(type: string, data: Record<string, unknown>): string {
|
||||||
|
const structuredData = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': type,
|
||||||
|
...data,
|
||||||
|
}
|
||||||
|
return JSON.stringify(structuredData)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization structured data (for homepage)
|
||||||
|
*/
|
||||||
|
export const organizationSchema = generateStructuredData('Organization', {
|
||||||
|
name: siteConfig.name,
|
||||||
|
url: siteConfig.url,
|
||||||
|
logo: `${siteConfig.url}/pounce-logo.png`,
|
||||||
|
description: siteConfig.description,
|
||||||
|
foundingDate: '2024',
|
||||||
|
sameAs: [
|
||||||
|
'https://twitter.com/pounce_domains',
|
||||||
|
'https://github.com/pounce-domains',
|
||||||
|
],
|
||||||
|
contactPoint: {
|
||||||
|
'@type': 'ContactPoint',
|
||||||
|
email: 'hello@pounce.ch',
|
||||||
|
contactType: 'customer service',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebApplication structured data (for the platform)
|
||||||
|
*/
|
||||||
|
export const webAppSchema = generateStructuredData('WebApplication', {
|
||||||
|
name: siteConfig.name,
|
||||||
|
url: siteConfig.url,
|
||||||
|
applicationCategory: 'BusinessApplication',
|
||||||
|
operatingSystem: 'Web Browser',
|
||||||
|
offers: {
|
||||||
|
'@type': 'AggregateOffer',
|
||||||
|
lowPrice: '0',
|
||||||
|
highPrice: '99',
|
||||||
|
priceCurrency: 'EUR',
|
||||||
|
offerCount: '3',
|
||||||
|
},
|
||||||
|
featureList: [
|
||||||
|
'Domain availability monitoring',
|
||||||
|
'TLD price comparison (886+ TLDs)',
|
||||||
|
'Domain portfolio management',
|
||||||
|
'Algorithmic domain valuation',
|
||||||
|
'Auction aggregation',
|
||||||
|
'Email notifications',
|
||||||
|
'Price alerts',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product structured data (for TLD detail pages)
|
||||||
|
*/
|
||||||
|
export function generateTldSchema(tld: string, avgPrice: number, description: string) {
|
||||||
|
return generateStructuredData('Product', {
|
||||||
|
name: `.${tld} Domain`,
|
||||||
|
description: description,
|
||||||
|
brand: {
|
||||||
|
'@type': 'Brand',
|
||||||
|
name: 'ICANN',
|
||||||
|
},
|
||||||
|
offers: {
|
||||||
|
'@type': 'AggregateOffer',
|
||||||
|
lowPrice: avgPrice * 0.7,
|
||||||
|
highPrice: avgPrice * 1.5,
|
||||||
|
priceCurrency: 'USD',
|
||||||
|
availability: 'https://schema.org/InStock',
|
||||||
|
},
|
||||||
|
aggregateRating: {
|
||||||
|
'@type': 'AggregateRating',
|
||||||
|
ratingValue: '4.5',
|
||||||
|
reviewCount: '100',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service structured data (for pricing page)
|
||||||
|
*/
|
||||||
|
export const serviceSchema = generateStructuredData('Service', {
|
||||||
|
serviceType: 'Domain Intelligence Service',
|
||||||
|
provider: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: siteConfig.name,
|
||||||
|
},
|
||||||
|
areaServed: 'Worldwide',
|
||||||
|
hasOfferCatalog: {
|
||||||
|
'@type': 'OfferCatalog',
|
||||||
|
name: 'Subscription Plans',
|
||||||
|
itemListElement: [
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
name: 'Scout (Free)',
|
||||||
|
price: '0',
|
||||||
|
priceCurrency: 'EUR',
|
||||||
|
description: 'Basic domain monitoring with 5 domains, daily checks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
name: 'Trader',
|
||||||
|
price: '19',
|
||||||
|
priceCurrency: 'EUR',
|
||||||
|
priceSpecification: {
|
||||||
|
'@type': 'UnitPriceSpecification',
|
||||||
|
price: '19',
|
||||||
|
priceCurrency: 'EUR',
|
||||||
|
billingDuration: 'P1M',
|
||||||
|
},
|
||||||
|
description: '50 domains, hourly checks, market insights',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offer',
|
||||||
|
name: 'Tycoon',
|
||||||
|
price: '49',
|
||||||
|
priceCurrency: 'EUR',
|
||||||
|
priceSpecification: {
|
||||||
|
'@type': 'UnitPriceSpecification',
|
||||||
|
price: '49',
|
||||||
|
priceCurrency: 'EUR',
|
||||||
|
billingDuration: 'P1M',
|
||||||
|
},
|
||||||
|
description: '500+ domains, 10-min checks, API access, bulk tools',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAQ structured data
|
||||||
|
*/
|
||||||
|
export const faqSchema = generateStructuredData('FAQPage', {
|
||||||
|
mainEntity: [
|
||||||
|
{
|
||||||
|
'@type': 'Question',
|
||||||
|
name: 'How does domain valuation work?',
|
||||||
|
acceptedAnswer: {
|
||||||
|
'@type': 'Answer',
|
||||||
|
text: 'Our algorithm calculates domain value using the formula: $50 × Length_Factor × TLD_Factor × Keyword_Factor × Brand_Factor. Each factor is based on market research and aftermarket data. See the full breakdown for any domain at /portfolio/valuation/{domain}.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Question',
|
||||||
|
name: 'How accurate is the TLD pricing data?',
|
||||||
|
acceptedAnswer: {
|
||||||
|
'@type': 'Answer',
|
||||||
|
text: 'We track prices from major registrars including Porkbun, Namecheap, GoDaddy, and Cloudflare. Prices are updated daily via automated scraping. We compare 886+ TLDs to help you find the best deals.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Question',
|
||||||
|
name: 'What is Smart Pounce?',
|
||||||
|
acceptedAnswer: {
|
||||||
|
'@type': 'Answer',
|
||||||
|
text: 'Smart Pounce is our auction aggregation feature. We scan GoDaddy, Sedo, NameJet, and other platforms to find domain auctions and analyze them for value. We don\'t handle payments — you bid directly on the platform.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Question',
|
||||||
|
name: 'Is there a free plan?',
|
||||||
|
acceptedAnswer: {
|
||||||
|
'@type': 'Answer',
|
||||||
|
text: 'Yes! Our Scout plan is free forever. You can monitor up to 5 domains with daily availability checks and access basic market insights.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BreadcrumbList structured data generator
|
||||||
|
*/
|
||||||
|
export function generateBreadcrumbs(items: { name: string; url: string }[]) {
|
||||||
|
return generateStructuredData('BreadcrumbList', {
|
||||||
|
itemListElement: items.map((item, index) => ({
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: index + 1,
|
||||||
|
name: item.name,
|
||||||
|
item: item.url,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default meta tags for all pages
|
||||||
|
*/
|
||||||
|
export const defaultMeta = {
|
||||||
|
title: getPageTitle(),
|
||||||
|
description: siteConfig.description,
|
||||||
|
keywords: siteConfig.keywords.join(', '),
|
||||||
|
author: siteConfig.author,
|
||||||
|
robots: 'index, follow',
|
||||||
|
'theme-color': siteConfig.themeColor,
|
||||||
|
'og:site_name': siteConfig.name,
|
||||||
|
'og:locale': siteConfig.locale,
|
||||||
|
'twitter:card': 'summary_large_image',
|
||||||
|
'twitter:site': siteConfig.twitter,
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user