linkbay-site 0.1.0
Static page management for marketing content. Compatible with LinkBayCMS ecosystem.
License: MIT
Requires Python: >=3.8
Keywords: cms, static-pages, fastapi, seo, multi-tenant, linkbay

# LinkBay-Site v0.1.0

[![License](https://img.shields.io/badge/license-MIT-blue)]() [![Python](https://img.shields.io/badge/python-3.8+-blue)]() 

Static page management for marketing content. Compatible with LinkBayCMS ecosystem.

## Caratteristiche

- ✅ **FastAPI nativo** con async/await
- ✅ **Multi-tenant** supporto completo
- ✅ **Template system** con layout multipli (full_width, sidebar, landing_page)
- ✅ **SEO-ready** con generazione automatica di sitemap.xml e robots.txt
- ✅ **Multi-language** supporto per traduzioni
- ✅ **Database agnostic** - funziona con qualsiasi ORM o database
- ✅ **Leggera e modulare** - zero dipendenze opzionali

## Prima di iniziare

Questa libreria è ideale se:
- Hai bisogno di gestire pagine statiche di marketing (about, contact, landing pages)
- Lavori con un'architettura multi-tenant
- Vuoi un sistema SEO-friendly con sitemap e robots.txt automatici
- Necessiti di supporto multi-lingua
- Preferisci una libreria leggera senza accoppiamento con database specifici

**Non è adatta** se cerchi un CMS completo con gestione media, blog, ecc. (per quello vedi LinkBayCMS completo)

## Installazione

```bash
pip install linkbay-site
```

Con supporto template Jinja2 e Markdown:

```bash
pip install linkbay-site[all]
```

Oppure l'ultima versione dal repository:

```bash
pip install git+https://github.com/AlessioQuagliara/LinkBay-Site
```

## Come funziona (3 passi)

### 1. Implementa il repository

La libreria usa il pattern Protocol per essere database-agnostic. Implementa il `PageRepositoryProtocol`:

```python
from linkbay_site.service import PageRepositoryProtocol
from typing import Optional, List, Dict, Any

class MyPageRepository(PageRepositoryProtocol):
    def __init__(self, db):
        self.db = db
    
    async def create(self, data: Dict[str, Any]) -> Any:
        # Usa il tuo ORM preferito (SQLAlchemy, Tortoise, etc.)
        page = await self.db.pages.create(**data)
        return page
    
    async def get_by_id(self, page_id: Any, tenant_id: str) -> Optional[Any]:
        return await self.db.pages.find_one(id=page_id, tenant_id=tenant_id)
    
    async def get_by_path(self, path: str, tenant_id: str, published_only: bool = False) -> Optional[Any]:
        query = {"path": path, "tenant_id": tenant_id}
        if published_only:
            query["published"] = True
        return await self.db.pages.find_one(**query)
    
    async def list_pages(self, tenant_id: str, skip: int = 0, limit: int = 100, published_only: bool = False) -> List[Any]:
        query = {"tenant_id": tenant_id}
        if published_only:
            query["published"] = True
        return await self.db.pages.find(query).skip(skip).limit(limit)
    
    async def update(self, page_id: Any, tenant_id: str, data: Dict[str, Any]) -> Optional[Any]:
        return await self.db.pages.update_one(
            {"id": page_id, "tenant_id": tenant_id},
            {"$set": data}
        )
    
    async def delete(self, page_id: Any, tenant_id: str) -> bool:
        result = await self.db.pages.delete_one(id=page_id, tenant_id=tenant_id)
        return result.deleted_count > 0
    
    async def get_all_published(self, tenant_id: str) -> List[Any]:
        return await self.db.pages.find(tenant_id=tenant_id, published=True)
```

### 2. Configura FastAPI

```python
from fastapi import FastAPI, Depends, Request, Response
from linkbay_site import PageService, create_page_router, create_public_router
from linkbay_site import SitemapGenerator, RobotsGenerator, TemplateRenderer

app = FastAPI()

# Inizializza il repository (con il tuo database)
page_repo = MyPageRepository(db)

# Crea il service
page_service = PageService(page_repository=page_repo)

# Dependency per ottenere il tenant corrente
async def get_current_tenant(request: Request) -> str:
    # Esempio: estrai tenant da header, domain, JWT, etc.
    return request.headers.get("X-Tenant-ID", "default")

# Router admin per CRUD
admin_router = create_page_router(
    service=page_service,
    get_current_tenant=get_current_tenant,
    prefix="/api/pages",
    tags=["Pages Admin"]
)
app.include_router(admin_router)

# Router pubblico per servire le pagine
public_router = create_public_router(
    service=page_service,
    get_tenant_from_request=get_current_tenant,
    template_renderer=TemplateRenderer(template_dir="templates"),
    prefix="",
    tags=["Public"]
)
app.include_router(public_router)

# SEO endpoints
@app.get("/sitemap.xml")
async def sitemap(tenant_id: str = Depends(get_current_tenant)):
    pages = await page_service.get_published_pages(tenant_id)
    generator = SitemapGenerator(base_url="https://example.com")
    xml = await generator.generate(pages)
    return Response(content=xml, media_type="application/xml")

@app.get("/robots.txt")
async def robots(tenant_id: str = Depends(get_current_tenant)):
    generator = RobotsGenerator(base_url="https://example.com")
    txt = await generator.generate_for_tenant(tenant_id)
    return Response(content=txt, media_type="text/plain")
```

### 3. Crea pagine

```python
from linkbay_site import PageCreate, LayoutType

# Crea una pagina
page_data = PageCreate(
    tenant_id="tenant1",
    path="/about",
    title="About Us",
    content="<h1>Welcome to our company</h1><p>We are awesome!</p>",
    meta_title="About Us - My Company",
    meta_description="Learn more about our company",
    layout=LayoutType.FULL_WIDTH,
    published=True
)

page = await page_service.create_page(page_data)
```

## Supporto Multi-lingua

```python
from linkbay_site import PageTranslationCreate

# Aggiungi traduzione italiana
translation = PageTranslationCreate(
    page_id=page.id,
    locale="it",
    title="Chi Siamo",
    content="<h1>Benvenuti nella nostra azienda</h1><p>Siamo fantastici!</p>",
    meta_title="Chi Siamo - La Mia Azienda",
    meta_description="Scopri di più sulla nostra azienda"
)

await page_service.create_translation(translation)

# Recupera pagina con traduzione
page = await page_service.get_page_by_path(
    path="/about",
    tenant_id="tenant1",
    locale="it"
)
```

## Template Personalizzati

### Con Jinja2

Crea templates in `templates/`:

```html
<!-- templates/full_width.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{{ meta_title }}</title>
    <meta name="description" content="{{ meta_description }}">
</head>
<body>
    <main>
        <h1>{{ title }}</h1>
        {{ content | safe }}
    </main>
</body>
</html>
```

### Con Custom Renderer

```python
def my_custom_renderer(layout: str, context: dict) -> str:
    # La tua logica di rendering
    return f"<html><body>{context['content']}</body></html>"

renderer = TemplateRenderer(custom_renderer=my_custom_renderer)
```

## Modelli Database (Esempio)

La libreria **NON include modelli** - questi sono solo esempi:

### SQLAlchemy

```python
from sqlalchemy import Column, String, Text, Boolean, DateTime, Integer
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Page(Base):
    __tablename__ = "pages"
    
    id = Column(Integer, primary_key=True)
    tenant_id = Column(String(255), nullable=False, index=True)
    path = Column(String(500), nullable=False)
    title = Column(String(500), nullable=False)
    content = Column(Text, nullable=False)
    meta_title = Column(String(500))
    meta_description = Column(Text)
    layout = Column(String(50), default="full_width")
    published = Column(Boolean, default=False)
    published_at = Column(DateTime)
    created_at = Column(DateTime)
    updated_at = Column(DateTime)

class PageTranslation(Base):
    __tablename__ = "page_translations"
    
    id = Column(Integer, primary_key=True)
    page_id = Column(Integer, nullable=False, index=True)
    locale = Column(String(10), nullable=False)
    title = Column(String(500), nullable=False)
    content = Column(Text, nullable=False)
    meta_title = Column(String(500))
    meta_description = Column(Text)
    created_at = Column(DateTime)
    updated_at = Column(DateTime)
```

## API Endpoints

### Admin (CRUD)

- `POST /api/pages/` - Crea pagina
- `GET /api/pages/` - Lista pagine
- `GET /api/pages/{page_id}` - Dettaglio pagina
- `PUT /api/pages/{page_id}` - Aggiorna pagina
- `DELETE /api/pages/{page_id}` - Elimina pagina
- `POST /api/pages/{page_id}/translations` - Aggiungi traduzione
- `GET /api/pages/{page_id}/translations` - Lista traduzioni

### Public

- `GET /{path}` - Serve pagina pubblicata

## Architettura

```
linkbay_site/
├── __init__.py          # Entry point con exports
├── schemas.py           # Pydantic models per validazione
├── service.py           # Business logic (Protocol-based)
├── router.py            # FastAPI routers (admin + public)
├── templates.py         # Template rendering system
└── seo.py              # Sitemap & robots.txt generators
```

## Ecosistema LinkBayCMS

Questa libreria fa parte dell'ecosistema LinkBayCMS:

- **LinkBay-Site** (questa) - Gestione pagine statiche
- **LinkBay-Auth** - Autenticazione multi-tenant
- **LinkBay-Media** - Gestione media e assets
- **LinkBay-Blog** - Sistema blog completo

## Licenza

MIT License - vedi [LICENSE](LICENSE)

## Contributi

I contributi sono benvenuti! Apri una issue o pull request.

## Supporto

- Issues: https://github.com/AlessioQuagliara/LinkBay-Site/issues
- Email: quagliara.alessio@gmail.com