# Backup di Notion su GitHub con API ufficiale e GitHub Actions

> Canonical: https://vxi.me/it/posts/notion-api-github-actions-backup/
> Locale: it-it
> Published: 2026-03-27
> Updated: 2026-03-27
> Markdown: https://vxi.me/it/posts/notion-api-github-actions-backup.md

Se hai mai provato a fare sul serio il backup di Notion, probabilmente hai incontrato uno di due problemi. O ti sei affidato a token non ufficiali del browser che prima o poi sono scaduti, oppure hai ottenuto un'export apparentemente riuscito e solo dopo ti sei accorto che aveva catturato soltanto la pagina principale, mentre sottopagine, database, allegati e nomi file erano ancora un caos.

La configurazione che ho finito per mantenere è molto più stabile: usare l'API ufficiale di Notion, darle una vera Internal Integration e lasciare che GitHub Actions esegua l'export a intervalli regolari. Questo elimina subito tre problemi comuni: niente più autenticazione basata su cookie, nessuna dipendenza da un computer locale che deve restare acceso e un posto pulito in cui estendere il flusso con ricorsione e rinomina dei file.

Se stai cercando cose come `Notion backup to GitHub`, `Notion GitHub Actions backup` o `export Notion to Markdown with the official API`, questo articolo parla esattamente di quel problema.

## Perché ho smesso di usare backup basati su token non ufficiali

I metodi non ufficiali possono funzionare, soprattutto per una prova rapida. Il problema arriva dopo.

Il workflow sembra andare bene finché non succede una di queste cose:

- la sessione del browser scade
- il token viene invalidato
- un IP o un ambiente diverso inizia a restituire 401 o 403
- l'export continua a partire, ma il perimetro dei dati è incompleto

L'API ufficiale sistema prima lo strato più fragile:

- l'autenticazione usa un token di Internal Integration, non una sessione del browser presa in prestito
- l'accesso alle pagine è esplicito, quindi sai perché una pagina è leggibile oppure no
- GitHub Actions fornisce scheduler e log prevedibili già pronti

Quello che devi ancora costruire è la parte pratica: attraversamento ricorsivo, gestione degli allegati e output leggibile.

## Come appare il flusso finale di backup

È più facile ragionare sull'intera catena guardandola dall'alto verso il basso:

```text
Pagina Notion principale
        ↓
Autorizzazione Internal Integration
        ↓
Esecuzione schedulata GitHub Actions
        ↓
Script Python con notion-client + notion2md
        ↓
Attraversamento ricorsivo di sottopagine e database figli
        ↓
Export Markdown + download allegati
        ↓
Estrazione ZIP
        ↓
Rinominare i file block-id con i veri titoli pagina
        ↓
Commit di tutto su GitHub
```

GitHub Actions qui è soltanto il runner. Il vero valore è nel livello Python intermedio, perché il percorso standard di esportazione non produce ancora un archivio ricorsivo completo nella forma che normalmente si vuole davvero.

## Sistema prima l'autorizzazione ufficiale

Parti dalla console sviluppatori di Notion e crea una Internal Integration:

1. Apri [Notion Integrations](https://www.notion.so/my-integrations)
2. Crea una nuova integrazione e scegli `Internal`
3. Concedile almeno `Read content`
4. Copia il token che inizia con `secret_`

Quel token da solo non basta. I permessi Notion sono espliciti, il che è positivo, ma significa anche che l'integrazione non può leggere una pagina finché non la colleghi a quella pagina.

Per la pagina principale che vuoi salvare:

1. apri la pagina in Notion
2. fai clic sul menu `...`
3. scegli `Add connections`
4. aggiungi l'integrazione appena creata

Se salti questo passaggio, il pattern di errore più comune è semplice:

- il token sembra valido
- il page ID sembra valido
- l'API continua a dire che il blocco non può essere trovato

Ti serve anche l'ID della pagina root. Di solito è l'ID da 32 caratteri presente nell'URL della pagina, con o senza trattini.

## Sul lato GitHub bastano due Secrets

Non inserirei i segreti direttamente nel file workflow. La versione pulita è salvarli come repository secrets:

- `NOTION_INTEGRATION_TOKEN`
- `NOTION_PAGE_ID`

Poi crea:

```text
.github/workflows/backup.yml
```

Il workflow esterno non deve essere complicato. Deve soltanto:

- fare checkout del repository
- configurare Python
- installare `notion-client` e `notion2md`
- eseguire lo script di export
- fare commit del risultato se il contenuto è cambiato

Una versione minimale è questa:

```yaml
name: "Notion Official API Backup"

on:
  schedule:
    - cron: "0 0 * * *"
  workflow_dispatch:

jobs:
  backup:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.10"

      - name: Install dependencies
        run: pip install notion2md notion-client

      - name: Run export
        env:
          NOTION_TOKEN: ${{ secrets.NOTION_INTEGRATION_TOKEN }}
          NOTION_PAGE_ID: ${{ secrets.NOTION_PAGE_ID }}
        run: python export_script.py

      - name: Commit changes
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action Backup"
          git add .
          if git diff --staged --quiet; then
            echo "No content changes"
          else
            git commit -m "Auto backup: $(date +'%Y-%m-%d %H:%M:%S')"
            git push
          fi
```

Questo basta a rendere reale l'automazione. La parte interessante rimane lo script di export.

## La parte importante è l'export ricorsivo, non il wrapper del workflow

Il problema tipico dell'export basato sull'API ufficiale non è che fallisca del tutto. È che si ferma troppo presto.

Se esporti una sola pagina, spesso finisci con:

- pagina root esportata
- sottopagine mancanti
- database figli trattati come problemi separati
- nomi file basati su block ID invece che su titoli leggibili

Per questo preferisco avvolgere l'export in uno script Python che gestisca esplicitamente:

- export Markdown della pagina corrente
- attraversamento delle sottopagine
- attraversamento delle pagine dentro i database figli
- estrazione ZIP degli asset scaricati
- rinomina da block ID a titoli pagina

Questa è la forma centrale che vale la pena conservare:

```python
import os
import zipfile
from notion_client import Client
from notion2md.exporter.block import MarkdownExporter

token = os.environ["NOTION_TOKEN"]
root_id = os.environ["NOTION_PAGE_ID"].replace("-", "")
notion = Client(auth=token)

def sanitize_title(title: str) -> str:
    safe = "".join(
        c for c in title
        if c.isalnum() or c in (" ", "-", "_", "（", "）", "【", "】", "、", ".")
    ).rstrip()
    return safe or "untitled"

def export_recursive(block_id: str, output_dir: str, title: str):
    block_id = block_id.replace("-", "")
    safe_title = sanitize_title(title)
    os.makedirs(output_dir, exist_ok=True)

    MarkdownExporter(block_id=block_id, output_path=output_dir, download=True).export()

    zip_path = os.path.join(output_dir, f"{block_id}.zip")
    if os.path.exists(zip_path):
        with zipfile.ZipFile(zip_path, "r") as zip_ref:
            zip_ref.extractall(output_dir)
        os.remove(zip_path)

    md_path = os.path.join(output_dir, f"{block_id}.md")
    new_md_path = os.path.join(output_dir, f"{safe_title}.md")
    if os.path.exists(md_path):
        if os.path.exists(new_md_path):
            os.remove(new_md_path)
        os.rename(md_path, new_md_path)

    results = []
    next_cursor = None
    while True:
        res = notion.blocks.children.list(block_id=block_id, start_cursor=next_cursor)
        results.extend(res.get("results", []))
        next_cursor = res.get("next_cursor")
        if not next_cursor:
            break

    child_dir = os.path.join(output_dir, safe_title)

    for block in results:
        if block["type"] == "child_page":
            export_recursive(block["id"], child_dir, block["child_page"]["title"])
        elif block["type"] == "child_database":
            db_dir = os.path.join(child_dir, sanitize_title(block["child_database"]["title"]))
            pages = notion.databases.query(database_id=block["id"]).get("results", [])
            for page in pages:
                page_title = "untitled"
                for _, prop in page["properties"].items():
                    if prop["type"] == "title" and prop["title"]:
                        page_title = prop["title"][0]["plain_text"]
                        break
                export_recursive(page["id"], db_dir, page_title)
```

È questo livello a trasformare un export basilare in un workflow di backup che puoi davvero mantenere.

## Perché è più manutenibile di `token_v2`

La differenza principale non è nel branding. È nella stabilità operativa.

Gli approcci in stile `token_v2` sono essenzialmente accesso temporaneo preso da una sessione browser autenticata. Una Internal Integration è invece una vera identità di servizio con permessi espliciti. È molto più adatta all'automazione non presidiata.

Una volta combinata con GitHub Actions, l'intera soluzione inizia a sembrare un vero sistema di manutenzione invece di un workaround.

## Dove è più probabile che il workflow si rompa

In pratica controllerei prima questi punti:

- il token in Secrets è sbagliato o contiene spazi indesiderati
- l'integrazione è stata creata ma non aggiunta alla pagina target
- il page ID è stato copiato male
- lo script funziona per la root ma fallisce durante ricorsione, estrazione ZIP o rinomina

Esiste anche il tema dei rate limit. Se il workspace contiene molte pagine annidate o molti allegati, una durata più lunga non significa automaticamente che il workflow sia rotto. A volte attraversamento e download sono semplicemente lenti.

Quindi, se GitHub Actions resta su `Run` per un po', controlla i log prima di concludere che l'intera pipeline sia morta.

## Cosa terrei d'occhio sul lungo periodo

Dopo il primo export riuscito, la domanda cambia da “può funzionare una volta?” a “continua a seguire nel tempo i cambiamenti reali?”.

Controllerei:

- che il repository continui a ricevere commit di backup regolari
- che le nuove sottopagine compaiano automaticamente
- che le voci dei database vengano esportate come previsto
- che la logica di rinomina gestisca bene lingue miste e caratteri speciali

Se dovessi migliorare ulteriormente il sistema, partirei da due piccoli upgrade:

1. rendere più espliciti i log di errore nell'output dell'Action
2. mantenere pulita la struttura del repository di backup, così l'archivio resta leggibile nel tempo

## Per chi è adatto e per chi no

È una buona soluzione se:

- usi Notion come vera knowledge base
- vuoi un percorso di backup stabile che non dipenda dai cookie del browser
- usi già GitHub e ti va bene che un repository conservi la cronologia degli export

Probabilmente è eccessivo se:

- esporti manualmente solo ogni tanto
- non vuoi toccare per niente GitHub Actions o Python
- non ti interessano sottopagine, database o una struttura di output più pulita

Se ti serve solo un export occasionale, la funzione integrata di Notion va ancora bene. L'automazione inizia a valere la pena quando le note sono cresciute abbastanza da trasformare l'export manuale in una routine che prima o poi finirai per saltare.
