new site
This commit is contained in:
@@ -0,0 +1,511 @@
|
||||
"""
|
||||
Export command for generating static files.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from ..services.supabase import SupabaseService
|
||||
from ..services.tags import TagAggregationService
|
||||
from ..services.images import ImageService
|
||||
from ..config import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
console = Console()
|
||||
|
||||
|
||||
def export_command(
|
||||
country: str,
|
||||
city: Optional[str] = None,
|
||||
output_dir: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Export static XML/HTML files for a country or city.
|
||||
|
||||
Generates company pages, city hubs, country hubs, and sitemaps
|
||||
from the database content.
|
||||
|
||||
Args:
|
||||
country: Country code (e.g., DE)
|
||||
city: Optional specific city slug
|
||||
output_dir: Optional output directory override
|
||||
"""
|
||||
console.print(f"\n[bold blue]Exporting files for {country}[/bold blue]\n")
|
||||
|
||||
settings = get_settings()
|
||||
db = SupabaseService()
|
||||
tags_service = TagAggregationService(db)
|
||||
image_service = ImageService()
|
||||
|
||||
# Setup output directory
|
||||
if output_dir:
|
||||
export_path = Path(output_dir)
|
||||
else:
|
||||
export_path = settings.export_dir
|
||||
|
||||
export_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Get country info
|
||||
country_obj = db.get_country_by_code(country.upper())
|
||||
if not country_obj:
|
||||
console.print(f"[red]Country not found: {country}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Setup Jinja environment
|
||||
jinja_env = _setup_jinja_environment(settings.templates_dir)
|
||||
|
||||
# Get cities to export
|
||||
all_cities = db.list_cities(country_id=country_obj.id)
|
||||
|
||||
if city:
|
||||
cities_to_export = [c for c in all_cities if c.slug == city]
|
||||
if not cities_to_export:
|
||||
console.print(f"[red]City not found: {city}[/red]")
|
||||
raise typer.Exit(1)
|
||||
else:
|
||||
cities_to_export = all_cities
|
||||
|
||||
console.print(f"Exporting {len(cities_to_export)} cities...")
|
||||
|
||||
# Export each city
|
||||
total_companies = 0
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
||||
console=console,
|
||||
) as progress:
|
||||
task = progress.add_task("Exporting...", total=len(cities_to_export))
|
||||
|
||||
for city_obj in cities_to_export:
|
||||
city_companies = _export_city(
|
||||
db=db,
|
||||
tags_service=tags_service,
|
||||
image_service=image_service,
|
||||
jinja_env=jinja_env,
|
||||
country=country_obj,
|
||||
city=city_obj,
|
||||
export_path=export_path,
|
||||
)
|
||||
total_companies += city_companies
|
||||
progress.advance(task)
|
||||
|
||||
# Export country hub
|
||||
_export_country_hub(
|
||||
db=db,
|
||||
tags_service=tags_service,
|
||||
jinja_env=jinja_env,
|
||||
country=country_obj,
|
||||
cities=cities_to_export,
|
||||
export_path=export_path,
|
||||
)
|
||||
|
||||
# Export sitemaps
|
||||
_export_sitemaps(
|
||||
db=db,
|
||||
country=country_obj,
|
||||
cities=cities_to_export,
|
||||
export_path=export_path,
|
||||
)
|
||||
|
||||
# Export tag JSON files for frontend filtering
|
||||
_export_tag_json(
|
||||
db=db,
|
||||
tags_service=tags_service,
|
||||
country=country_obj,
|
||||
cities=cities_to_export,
|
||||
export_path=export_path,
|
||||
)
|
||||
|
||||
console.print(f"\n[bold green]Export complete![/bold green]")
|
||||
console.print(f" - Companies: {total_companies}")
|
||||
console.print(f" - Cities: {len(cities_to_export)}")
|
||||
console.print(f" - Output: {export_path}")
|
||||
|
||||
|
||||
def _setup_jinja_environment(templates_dir: Path) -> Environment:
|
||||
"""Setup Jinja2 templating environment."""
|
||||
# Create templates directory if it doesn't exist
|
||||
templates_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create default templates if they don't exist
|
||||
_ensure_templates_exist(templates_dir)
|
||||
|
||||
return Environment(
|
||||
loader=FileSystemLoader(str(templates_dir)),
|
||||
autoescape=True,
|
||||
)
|
||||
|
||||
|
||||
def _ensure_templates_exist(templates_dir: Path) -> None:
|
||||
"""Create default templates if they don't exist."""
|
||||
# Company template
|
||||
company_template = templates_dir / "company.xml"
|
||||
if not company_template.exists():
|
||||
company_template.write_text(_DEFAULT_COMPANY_TEMPLATE)
|
||||
|
||||
# City hub template
|
||||
city_template = templates_dir / "city_hub.xml"
|
||||
if not city_template.exists():
|
||||
city_template.write_text(_DEFAULT_CITY_TEMPLATE)
|
||||
|
||||
# Country hub template
|
||||
country_template = templates_dir / "country_hub.xml"
|
||||
if not country_template.exists():
|
||||
country_template.write_text(_DEFAULT_COUNTRY_TEMPLATE)
|
||||
|
||||
|
||||
def _export_city(
|
||||
db: SupabaseService,
|
||||
tags_service: TagAggregationService,
|
||||
image_service: ImageService,
|
||||
jinja_env: Environment,
|
||||
country,
|
||||
city,
|
||||
export_path: Path,
|
||||
) -> int:
|
||||
"""Export all files for a city."""
|
||||
# Create city directory
|
||||
city_path = export_path / country.slug / city.slug
|
||||
city_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Get companies
|
||||
companies = db.list_companies(
|
||||
city_id=city.id,
|
||||
status="content_generated",
|
||||
limit=500,
|
||||
)
|
||||
|
||||
# Export each company
|
||||
company_template = jinja_env.get_template("company.xml")
|
||||
|
||||
for company in companies:
|
||||
company_tags = db.get_company_tags(company.id)
|
||||
|
||||
context = {
|
||||
"company": company,
|
||||
"city": city,
|
||||
"country": country,
|
||||
"tags": company_tags,
|
||||
"services": [t for t in company_tags if hasattr(t, "category") and t.category == "service"],
|
||||
"industries": [t for t in company_tags if hasattr(t, "category") and t.category == "industry"],
|
||||
"modules": [t for t in company_tags if hasattr(t, "category") and t.category == "module"],
|
||||
}
|
||||
|
||||
content = company_template.render(**context)
|
||||
company_file = city_path / f"{company.slug}.html"
|
||||
company_file.write_text(content, encoding="utf-8")
|
||||
|
||||
# Export city hub
|
||||
city_tags = tags_service.get_city_tags_by_category(city.id)
|
||||
hub_template = jinja_env.get_template("city_hub.xml")
|
||||
|
||||
hub_context = {
|
||||
"city": city,
|
||||
"country": country,
|
||||
"companies": companies,
|
||||
"company_count": len(companies),
|
||||
"tags": city_tags,
|
||||
}
|
||||
|
||||
hub_content = hub_template.render(**hub_context)
|
||||
hub_file = city_path / "index.html"
|
||||
hub_file.write_text(hub_content, encoding="utf-8")
|
||||
|
||||
return len(companies)
|
||||
|
||||
|
||||
def _export_country_hub(
|
||||
db: SupabaseService,
|
||||
tags_service: TagAggregationService,
|
||||
jinja_env: Environment,
|
||||
country,
|
||||
cities: List,
|
||||
export_path: Path,
|
||||
) -> None:
|
||||
"""Export country hub page."""
|
||||
country_path = export_path / country.slug
|
||||
country_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Get aggregate data
|
||||
total_companies = 0
|
||||
city_data = []
|
||||
|
||||
for city in cities:
|
||||
count = db.count_companies(city_id=city.id, status="content_generated")
|
||||
total_companies += count
|
||||
city_data.append({
|
||||
"city": city,
|
||||
"company_count": count,
|
||||
})
|
||||
|
||||
# Sort cities by company count
|
||||
city_data.sort(key=lambda x: x["company_count"], reverse=True)
|
||||
|
||||
country_tags = tags_service.get_country_tags_by_category(country.id)
|
||||
|
||||
# Render template
|
||||
template = jinja_env.get_template("country_hub.xml")
|
||||
content = template.render(
|
||||
country=country,
|
||||
cities=city_data,
|
||||
city_count=len(cities),
|
||||
company_count=total_companies,
|
||||
tags=country_tags,
|
||||
)
|
||||
|
||||
hub_file = country_path / "index.html"
|
||||
hub_file.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def _export_sitemaps(
|
||||
db: SupabaseService,
|
||||
country,
|
||||
cities: List,
|
||||
export_path: Path,
|
||||
) -> None:
|
||||
"""Export XML sitemaps."""
|
||||
sitemap_path = export_path / "sitemaps"
|
||||
sitemap_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
base_url = "https://odoo-expertos.com"
|
||||
|
||||
# Company sitemap
|
||||
urls = []
|
||||
for city in cities:
|
||||
companies = db.list_companies(
|
||||
city_id=city.id,
|
||||
status="content_generated",
|
||||
limit=500,
|
||||
)
|
||||
for company in companies:
|
||||
urls.append(
|
||||
f"{base_url}/{country.slug}/{city.slug}/{company.slug}.html"
|
||||
)
|
||||
|
||||
sitemap_xml = _generate_sitemap_xml(urls)
|
||||
sitemap_file = sitemap_path / f"sitemap-companies-{country.code.lower()}.xml"
|
||||
sitemap_file.write_text(sitemap_xml, encoding="utf-8")
|
||||
|
||||
# City sitemap
|
||||
city_urls = [
|
||||
f"{base_url}/{country.slug}/{city.slug}/"
|
||||
for city in cities
|
||||
]
|
||||
city_sitemap = _generate_sitemap_xml(city_urls)
|
||||
city_sitemap_file = sitemap_path / f"sitemap-cities-{country.code.lower()}.xml"
|
||||
city_sitemap_file.write_text(city_sitemap, encoding="utf-8")
|
||||
|
||||
|
||||
def _generate_sitemap_xml(urls: List[str]) -> str:
|
||||
"""Generate sitemap XML content."""
|
||||
lines = ['<?xml version="1.0" encoding="UTF-8"?>']
|
||||
lines.append('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">')
|
||||
|
||||
for url in urls:
|
||||
lines.append(" <url>")
|
||||
lines.append(f" <loc>{url}</loc>")
|
||||
lines.append(" <changefreq>monthly</changefreq>")
|
||||
lines.append(" <priority>0.7</priority>")
|
||||
lines.append(" </url>")
|
||||
|
||||
lines.append("</urlset>")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _export_tag_json(
|
||||
db: SupabaseService,
|
||||
tags_service: TagAggregationService,
|
||||
country,
|
||||
cities: List,
|
||||
export_path: Path,
|
||||
) -> None:
|
||||
"""Export tag aggregations as JSON for frontend filtering."""
|
||||
json_path = export_path / "tags"
|
||||
json_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# City tags
|
||||
cities_path = json_path / "cities"
|
||||
cities_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for city in cities:
|
||||
city_json = tags_service.export_city_tags_json(city.id)
|
||||
json_file = cities_path / f"{city.slug}.json"
|
||||
json_file.write_text(json.dumps(city_json, indent=2), encoding="utf-8")
|
||||
|
||||
# Country tags
|
||||
country_json = tags_service.export_country_tags_json(country.id)
|
||||
country_file = json_path / f"{country.slug}.json"
|
||||
country_file.write_text(json.dumps(country_json, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
# Default templates
|
||||
|
||||
_DEFAULT_COMPANY_TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="{{ country.language }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ company.meta_title or company.name }}</title>
|
||||
<meta name="description" content="{{ company.meta_description or company.description }}">
|
||||
<link rel="canonical" href="https://odoo-expertos.com/{{ country.slug }}/{{ city.slug }}/{{ company.slug }}.html">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/{{ country.slug }}/">{{ country.name }}</a> >
|
||||
<a href="/{{ country.slug }}/{{ city.slug }}/">{{ city.name }}</a> >
|
||||
{{ company.name }}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<article>
|
||||
<h1>{{ company.name }}</h1>
|
||||
|
||||
{% if company.description %}
|
||||
<p class="lead">{{ company.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="company-info">
|
||||
{% if company.address %}
|
||||
<p><strong>Address:</strong> {{ company.address }}</p>
|
||||
{% endif %}
|
||||
{% if company.phone %}
|
||||
<p><strong>Phone:</strong> {{ company.phone }}</p>
|
||||
{% endif %}
|
||||
{% if company.website %}
|
||||
<p><strong>Website:</strong> <a href="{{ company.website }}" rel="noopener">{{ company.website }}</a></p>
|
||||
{% endif %}
|
||||
{% if company.email %}
|
||||
<p><strong>Email:</strong> {{ company.email }}</p>
|
||||
{% endif %}
|
||||
{% if company.rating %}
|
||||
<p><strong>Rating:</strong> {{ company.rating }}/5 ({{ company.review_count }} reviews)</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
{{ company.content_html | safe }}
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© Odoo Expertos - {{ city.name }}, {{ country.name }}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
_DEFAULT_CITY_TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="{{ country.language }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Odoo Partners in {{ city.name }} | {{ country.name }}</title>
|
||||
<meta name="description" content="Find {{ company_count }} certified Odoo partners in {{ city.name }}, {{ country.name }}. Compare services, expertise, and reviews.">
|
||||
<link rel="canonical" href="https://odoo-expertos.com/{{ country.slug }}/{{ city.slug }}/">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/{{ country.slug }}/">{{ country.name }}</a> >
|
||||
{{ city.name }}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<h1>Odoo Partners in {{ city.name }}</h1>
|
||||
<p>Discover {{ company_count }} certified Odoo partners in {{ city.name }}, {{ country.name }}.</p>
|
||||
|
||||
<section class="filters">
|
||||
<h2>Filter by Services</h2>
|
||||
<ul>
|
||||
{% for tag in tags.services %}
|
||||
<li>{{ tag.name }} ({{ tag.count }})</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="companies">
|
||||
<h2>Odoo Partners</h2>
|
||||
{% for company in companies %}
|
||||
<article class="company-card">
|
||||
<h3><a href="{{ company.slug }}.html">{{ company.name }}</a></h3>
|
||||
{% if company.description %}
|
||||
<p>{{ company.description }}</p>
|
||||
{% endif %}
|
||||
{% if company.rating %}
|
||||
<p class="rating">{{ company.rating }}/5</p>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© Odoo Expertos - {{ city.name }}, {{ country.name }}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
_DEFAULT_COUNTRY_TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="{{ country.language }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Odoo Partners in {{ country.name }}</title>
|
||||
<meta name="description" content="Find {{ company_count }} certified Odoo partners in {{ city_count }} cities across {{ country.name }}.">
|
||||
<link rel="canonical" href="https://odoo-expertos.com/{{ country.slug }}/">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
{{ country.name }}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<h1>Odoo Partners in {{ country.name }}</h1>
|
||||
<p>Discover {{ company_count }} certified Odoo partners in {{ city_count }} cities across {{ country.name }}.</p>
|
||||
|
||||
<section class="cities">
|
||||
<h2>Cities</h2>
|
||||
<ul>
|
||||
{% for item in cities %}
|
||||
<li>
|
||||
<a href="{{ item.city.slug }}/">{{ item.city.name }}</a>
|
||||
({{ item.company_count }} partners)
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="filters">
|
||||
<h2>Services</h2>
|
||||
<ul>
|
||||
{% for tag in tags.services %}
|
||||
<li>{{ tag.name }} ({{ tag.company_count }} companies in {{ tag.city_count }} cities)</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© Odoo Expertos - {{ country.name }}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
Reference in New Issue
Block a user