# Schoool — CLAUDE.md

## Progetto
Sito Drupal 10. Piattaforma formativa (corsi, risorse, blog).

## Strategia branch

Solo due branch attivi: **`main`** (produzione) e **`dev`** (sviluppo).

- Sviluppare su `dev`, aprire PR da `dev` → `main`
- Dopo ogni merge su `main`, sincronizzare dev: `git checkout dev && git merge main && git push origin dev`
- **Non creare branch `chore/xxx`, `feature/xxx` o simili** — il progetto è abbastanza semplice da non richiederne

## Ambiente di sviluppo
Il progetto gira in **DDEV** (Docker). Il PHP usato da drush e composer è quello del container, non quello dell'host.

```bash
# Comandi drush: SEMPRE tramite DDEV
ddev drush cr
ddev drush en <modulo> -y
ddev drush config:import --partial -y

# Composer: eseguito sull'host (PHP host deve funzionare)
composer require drupal/<pacchetto>
```

**Nota PHP host:** se `composer` fallisce con errore `libicuio.74.dylib`, eseguire `brew reinstall php` per riallineare PHP all'icu4c corrente. Il PHP del container DDEV non è affetto da questo problema.

## Tema
- Nome: `tailwindcss` (tema contribuito adattato)
- Percorso: `web/themes/custom/tailwindcss/`
- Base theme: `stable` (Bootstrap Barrio è disattivato)
- Admin theme: Gin
- CSS framework: Tailwind CSS v3

## Componenti SDC
I componenti header e footer sono Single Directory Components (SDC) di Drupal.
- `components/header/` — sticky header con logo SVG, nav desktop, CTA button, drawer mobile
- `components/footer/` — footer con newsletter, sitemap, sub-footer

### Regola full-bleed
**Solo il footer** usa la tecnica full-bleed (`width: 100vw; position: relative; left: 50%; margin-left: -50vw`).
`html { overflow-x: hidden }` è impostato in `css/components/page.css`.

**L'header NON usa full-bleed** — usa `width: 100%`. Motivo: è sempre fuori dai wrapper con padding/margin, e il full-bleed causava sovrapposizione con la sidebar dell'admin theme Gin.

### Regola posizionamento header/footer nei template
I componenti SDC header e footer devono stare **FUORI** dal wrapper `<div{{ attributes.addClass(classes) }}>` che riceve la classe `m-16` da `tailwindcss_preprocess_page()` quando l'utente è loggato.

### Sistema padding mobile (footer)
- `--footer-padding-inline: 1rem` su mobile, `0` su desktop (≥ 1024px)

## Design system
Ispirato al blueprint visivo jonas.io. Classi e token definiti in:
- `css/client-overrides.css` — token CSS (colori, font, spacing)
- `css/components/design-system.css` — utility classes (container, sezioni, bottoni, card, layout)

### Token principali
```css
--client-color-primary: #37a169    /* brand green */
--client-color-heading: #1a1a1a
--client-color-text: #333333
--client-color-muted: #888888
--client-color-bg-alt: #f3f3f0     /* sezioni alternate */
--client-container-max: 1100px
--client-section-py: 6rem
```

### Classi container e sezioni
```html
<div class="site-container">          <!-- max 1100px, px-6/lg:px-12 -->
<section class="section">             <!-- py-24 -->
<section class="section section-alt"> <!-- py-24 + bg #f3f3f0 -->
```

### Bottoni
```html
<a class="btn-site btn-primary">  <!-- verde, rounded-full -->
<a class="btn-site btn-ghost">    <!-- bordo, trasparente -->
```

## Struttura page templates principali
- `page--front.html.twig` — home page (hero, feature sections dinamiche via block_content, testimonianze, CTA)
- `page--node.html.twig` — nodi generici; `page.header` wrapped in `site-container text-center`
- `page--corso.html.twig` — nodo corso: mostra lista lezioni sopra il contenuto (se presenti)
- `page--corsi.html.twig` — lista corsi (route `view.corsi.page_1`)
- `page--articoli.html.twig` — lista articoli (route `view.articoli.page_1`)
- `page--blog.html.twig` — blog
- `page--landing.html.twig` — landing page
- `page--area-studente.html.twig` — area studente (route `schoool_corsi.area_studente`)
- `page--dashboard--studente.html.twig` — dashboard studente (`/dashboard/studente`)
- `page--dashboard--editor.html.twig` — dashboard editor (`/dashboard/editor`) — lista corsi
- `page--dashboard--editor--corso.html.twig` — gestione lezioni corso (`/dashboard/editor/corso/{nid}`)
- `page--dashboard--admin.html.twig` — dashboard admin (`/dashboard/admin`)
- `page--dashboard--editor--iscritti.html.twig` — iscritti ai corsi dell'editor
- `page--dashboard--editor--ordini.html.twig` — ordini per i corsi dell'editor
- `page.html.twig` — default

**Percorso template:** `templates/layout/page/`

**Theme suggestions views** in `tailwindcss_theme_suggestions_page_alter()`:
```php
'view.corsi.page_1'    → 'page__corsi'
'view.articoli.page_1' → 'page__articoli'
```

## Sistema corsi (Commerce + License)

### Architettura
```
corso (node) ← referenziato da → commerce_product tipo "corso"
                                        ↓ variation con trait commerce_license
                                  ORDER COMPLETED
                                        ↓
                              CorsoAccess license (plugin corso_access)
                              campo field_licensed_corso → node.corso
                                        ↓
                    lezione (node) ← hook_node_access verifica licenza
                                        ↓
                              /area-studente/{corso} — dashboard
```

### Content types
- **`corso`** — immagine copertina, durata (text), instructor (text), body
- **`lezione`** — `field_lezione_corso` (ref→corso, required), `field_lezione_video` (link), `field_lezione_peso` (integer, ordine)

### Commerce
- Product type: `corso` / variation type: `corso`
- Variation type usa il trait `commerce_license` (aggiunge `license_type` e `license_expiration`)
- **ATTENZIONE:** dopo config import, il trait va installato esplicitamente via PHP:
  ```php
  $tm = Drupal::service('plugin.manager.commerce_entity_trait');
  $tm->installTrait($tm->createInstance('commerce_license'), 'commerce_product_variation', 'corso');
  ```
- Campo prodotto: `field_product_corso_ref` → node.corso

### Modulo custom `schoool_corsi`
**Percorso:** `web/modules/custom/schoool_corsi/`

- **`CorsoAccess`** plugin (`id = "corso_access"`) — `src/Plugin/Commerce/LicenseType/CorsoAccess.php`
  - Bundle field sulla licenza: `field_licensed_corso` → node.corso
  - `grantLicense()` / `revokeLicense()` = noop (accesso dinamico)
- **`hook_node_access()`** in `schoool_corsi.module` — protegge nodi `lezione` (op=view, non admin)
  - Verifica: licenza attiva con `type=corso_access`, `state=active`, `field_licensed_corso=corso_nid`
- **`AreaStudenteController`** — route `/area-studente/{node}` (node = corso)
  - Access check via licenza attiva
  - Render: lista lezioni ordinate per `field_lezione_peso` + view `commerce_user_orders`
- **`hook_theme_suggestions_page_alter()`** — aggiunge suggestion `page__area_studente` e tutte le dashboard
- **`StudenteDashboardController`** — `/dashboard/studente`, carica tutte le licenze `corso_access` dell'utente (qualsiasi stato)
- **`EditorDashboardController`**:
  - `view()` → `/dashboard/editor` — lista corsi pulita, quick links (Nuovo Corso, Iscritti, Ordini)
  - `corsoDetail(NodeInterface $node)` → `/dashboard/editor/corso/{nid}` — lezioni con tabledrag + tasto Salva
  - `iscritti()`, `ordini()`, `reorder()` — pagine secondarie
- **`AdminDashboardController`** — extends Editor, theme `admin_dashboard`, aggiunge quick links globali
- **Permessi**: `access editor dashboard`, `access admin dashboard`, `reorder lezioni`
- **Tabledrag**: `drupalSettings.tableDrag` generato dal controller PHP; chiave = `name` dell'input (`lezione_peso_{nid}`); `<tr>` classe `draggable`; JS custom gestisce tasto "Salva ordine" → AJAX
- **Pre-popolazione lezione**: `hook_form_node_lezione_form_alter` legge `?field_lezione_corso[0][target_id]` e imposta `#default_value` — pattern per pre-popolare entity_reference da URL
- **Dashboard URL in header**: `dashboard_url` calcolato in `tailwindcss_preprocess_page()`, passato all'SDC header

### Header — Area Corsi
- Desktop: `.sdc-header__dashboard-link` nel nav, `display: inline-flex` (≥1024px)
- Mobile: `.sdc-header__dashboard-bar` — barra verde dentro `<header>` dopo `.sdc-header__inner`, visibile direttamente senza aprire hamburger; nascosta a ≥1024px

### Config import con nuovi moduli
Quando si installa un nuovo modulo via `ddev drush en`:
1. Aggiornare `config/sync/core.extension.yml` (aggiungere il modulo)
2. Copiare le config del modulo da DB a sync via PHP eval:
   ```php
   $sync->write($name, $active->read($name));
   ```
3. `ddev drush config:import --partial -y`

## Librerie principali
```yaml
global-styling:
  css:
    component:
      css/components/design-system.css: { weight: -10 }
      dist/custom.css: { weight: 2 }
      css/client-overrides.css: { weight: 99 }
    base:
      dist/tailwind.css: {}
global-interactions:
  js: js/header-drawer.js
```

## Views grid (corsi / articoli)

Il tema `stable` NON aggiunge `view-id-*` né `view-content` al wrapper delle view. Soluzione obbligatoria: override di `views-view-unformatted--[view_id].html.twig` che aggiunge un wrapper con classe nota.

```
templates/views/views-view-unformatted--corsi.html.twig    → <div class="corsi-grid">
templates/views/views-view-unformatted--articoli.html.twig → <div class="articoli-grid">
```

CSS in `design-system.css`: griglia responsive con `.views-row { display: contents }`.
Le card (`.corso-card`, `.article-card`) devono avere `width: 100%` esplicito.

### Card teaser articoli — pitfall
- Image wrap: usare **`<div>`**, NON `<a>` — l'image formatter Drupal aggiunge il suo `<a>` creando nesting invalido
- Condizione immagine: `{% if node.field_immagine is not empty %}` — `.entity` su FieldItemList vuoto è truthy → immagine rotta
- Primo tag: accedere tramite `node.field_tags.0.entity.label`, non `content.field_tags`
- `{{ product.field_image }}` / `{{ content.field_immagine }}` usano il formatter del display → può essere `entity_reference_label` (solo testo). Accedere sempre direttamente all'entità media

### Stable theme — CSS pitfall
Il tema `stable` non aggiunge classi CSS ai field wrapper (niente `.field__item`, `.field`).
Il selettore corretto per il testo dentro un field è `> div` (figlio diretto).

## Campi immagine (media)

I campi immagine sono stati migrati a `entity_reference` → `media` (media type `image`, source field `field_media_image`).

- **commerce_product corso**: `field_image` (entity_reference → media)
- **node article**: `field_immagine` (entity_reference → media)

**Pattern Twig (richiede `twig_tweak` installato):**
```twig
{% if node.field_immagine is not empty %}
  {% set media = node.field_immagine.entity %}
  {% if media %}
    {% set img = media.field_media_image %}
    <img src="{{ img.entity.uri.value|image_style('large') }}" alt="{{ img.alt }}" />
  {% endif %}
{% endif %}
```

**PITFALL**: `{% if node.field_xxx.entity %}` è truthy anche a campo vuoto → broken img. Usare sempre `is not empty`.

## Feature Sections — home page dinamica

Le sezioni 01/02/03 della home page sono gestite via **block content type `feature_section`**.

### Campi del tipo
- `field_fs_heading` — titolo (string, obbligatorio)
- `field_fs_body` — testo (text_long)
- `field_fs_cta_primary` — link con etichetta (obbligatorio)
- `field_fs_cta_secondary` — link con etichetta (opzionale)
- `field_immagine` — entity_reference → media (storage condiviso con altri bundle)
- `field_fs_peso` — integer, ordine di visualizzazione

### Caricamento in preprocess
`tailwindcss_preprocess_page()` carica tutti i `feature_section` ordinati per `field_fs_peso` ASC e li passa come `$variables['feature_sections']`.

### Regola alternanza immagine
Il loop in `page--front.html.twig` usa `loop.index`:
- Dispari → `class="feature-row feature-row-reverse"` → immagine **sinistra**
- Pari → `class="feature-row"` → immagine **destra**

**ATTENZIONE**: `feature-row-reverse` NON ha `display:flex` da solo — va sempre combinato con `feature-row`. Usare sempre entrambe le classi insieme per il layout invertito.

### Gestione contenuti
Creare/modificare blocchi su `/block/add/feature_section`. Ordinare tramite il campo `Ordine (peso)`.

## Article node template
`templates/layout/node/node--article.html.twig` — layout:
1. Hero image full-width (`.article-single__hero`) — campo `field_immagine` (media)
2. Reading area centrata max 720px (`.article-single__reading`)
   - `.article-single__meta` — autore/data italic
   - `.article-single__body` — font 1.125rem, line-height 1.85
   - `.article-single__tags` — tags inline italic (via `field--node--field-tags.html.twig`)

## Header — cart block e user menu
- **Cart block**: passato via `tailwindcss_preprocess_page()` con check `moduleExists('commerce_cart')`
- Icona nera: `tailwindcss_preprocess_commerce_cart_block()` sovrascrive `$variables['icon']`
- Template override Commerce: `templates/commerce/commerce-cart-block.html.twig` — mostra solo `{{ count }}` (numero), nasconde se 0
- Su mobile: `.cart-block--summary__count` e `.sdc-header__user-label` nascosti (solo icone); ripristinati su desktop (≥1024px)
- **User menu** (`.sdc-header__user`): link a `/user/login` (anonimo) o `/user` (loggato), sempre visibile. Usa `{{ logged_in }}` da contesto Twig.

## Lezioni corso (page--corso.html.twig)

Il template `page--corso.html.twig` mostra le lezioni del corso in cima alla pagina, sopra `page.content`.

### Come funziona
`tailwindcss_preprocess_page()` carica le lezioni solo quando il nodo corrente è di tipo `corso`:
```php
$lezione_ids = \Drupal::entityQuery('node')
  ->condition('type', 'lezione')
  ->condition('field_lezione_corso', $route_node->id())
  ->condition('status', 1)
  ->sort('field_lezione_peso', 'ASC')
  ->accessCheck(FALSE)
  ->execute();
$variables['lezioni'] = $lezione_ids ? array_values(...loadMultiple($lezione_ids)) : [];
```

Il Twig mostra la sezione solo se `lezioni|length > 0`. Ogni riga:
- Numero circolare verde (`.corso-lezioni-num`)
- Link al nodo lezione (`.corso-lezioni-title`)
- Icona play SVG se `field_lezione_video is not empty`

CSS: classi `.corso-lezioni-*` in `css/components/design-system.css`.

## Deploy produzione

**Metodo: sincronizzazione diretta** (PhpStorm Deployment / rsync). Il workflow GitHub Actions (`deploy-production.yml`) è **disabilitato** — non usare CI su questo hosting.

> **Perché no CI:** InMotion/cPanel con CageFS e PHP-FPM non permette restart del pool PHP-FPM dall'utente. Deploy con symlink causano "Failed opening required index.php" per via dell'open_basedir che non segue i symlink. Il rollback via symlink fallisce se il DB è già stato migrato. Il deploy manuale è più sicuro e controllabile in questo contesto.

### Repo GitHub
`https://github.com/Schoool-org/schoool.git` (organizzazione Team)

### Struttura server
- Hosting InMotion/cPanel — home in `$HOME`, web root in `$HOME/public_html/`
- **Document root cPanel**: `public_html/schoool_site/web`
- PHP 8.3: `/opt/cpanel/ea-php83/root/usr/bin/php`
- **PHP handler cPanel** (Apache Handlers): `application/x-httpd-ea-php83` per `.php .php8 .phtml`
- **open_basedir** (MultiPHP INI Editor): `/home/apartm29/public_html/:/home/apartm29/schoool/:/tmp/`

### Sync con PhpStorm
Deployment → Sync with Deployed.

| Local | Remote |
|---|---|
| `/` (root progetto) | `public_html/schoool_site/` |
| `web/sites/default/files/` | `public_html/schoool_site/web/sites/default/files/` |

**Escludi sempre dalla sync:**
- `web/sites/default/settings.php`
- `web/sites/default/files/`
- `web/sites/default/private/`
- `.ddev/`
- `node_modules/`
- `.git/`

### Flusso di lavoro quotidiano

**Modifiche template / CSS / JS (senza config):**
```
1. Sviluppa in DDEV
2. npm run build  (se CSS cambiato)
3. PhpStorm → Deployment → Sync with Deployed
```

**Modifiche che coinvolgono configurazioni Drupal:**
```bash
# 1. Esporta config dal DDEV locale
ddev drush config:export

# 2. Commit + push
git add config/sync/
git commit -m "config: descrizione modifica"
git push

# 3. Sul server — pull + import
cd ~/public_html/schoool_site
git pull origin main
vendor/bin/drush --root=web --uri="https://lavorosicuramente.com" config:import -y
vendor/bin/drush --root=web --uri="https://lavorosicuramente.com" cache:rebuild
```

**Nuovi moduli contrib (composer):**

> **PITFALL drush**: `drush/drush` deve stare SEMPRE in `require`, mai in `require-dev`. In produzione si usa `composer install --no-dev` → i pacchetti dev vengono ignorati → drush non disponibile.

```bash
# 1. Locale
composer require drupal/<modulo>
ddev drush en <modulo> -y
ddev drush config:export
git add composer.json composer.lock config/sync/
git commit -m "feat: aggiungi modulo X"
git push

# 2. Sul server
cd ~/public_html/schoool_site
git pull origin main
composer install --no-dev
vendor/bin/drush --root=web --uri="https://lavorosicuramente.com" updatedb -y
vendor/bin/drush --root=web --uri="https://lavorosicuramente.com" config:import -y
vendor/bin/drush --root=web --uri="https://lavorosicuramente.com" cache:rebuild
```

### Checklist PR — modifiche config Drupal

0. **Prima di aprire la PR**, in dev:
   ```bash
   ddev drush cst
   ```
   Se mostra differenze, hai config nel DB non ancora esportata → esegui `cex -y` prima di procedere.
1. In dev hai eseguito `ddev drush cex -y` dopo le modifiche UI.
2. La PR contiene solo ciò che vuoi deployare (controlla `config/sync/` in "Files changed").
3. Nessun file sensibile/env-specifico (`settings.local.php`, `services.local.yml`, `files/`, ecc.).
4. Titolo PR chiaro: `config: ...` oppure `config+code: ...`.
5. Dopo merge su `main`, in prod esegui **sempre**:
   ```bash
   git pull origin main
   vendor/bin/drush --root=web cim -y
   vendor/bin/drush --root=web cr
   vendor/bin/drush --root=web cst
   ```
   Se `cst` mostra differenze dopo `cim`, fermati: significa drift o override runtime da correggere prima del prossimo deploy.

### Rollback configurazioni

Il rollback è un semplice `git` — le config sono in git, quindi:

```bash
# Se git pull fallisce con "unstaged changes" (modifiche locali non committate):
git restore <file>      # scarta le modifiche locali
git pull origin main

# Sul server — torna a un commit precedente della sola config
git log --oneline config/sync/          # individua il commit target
git checkout <hash> -- config/sync/
vendor/bin/drush --root=web --uri="https://lavorosicuramente.com" config:import -y
vendor/bin/drush --root=web --uri="https://lavorosicuramente.com" cache:rebuild
```

### Procedura primo deploy (o re-deploy da zero)
```bash
# 1. Dump DB da DDEV locale
ddev drush sql-dump --gzip > /tmp/db.sql.gz
scp /tmp/db.sql.gz user@server:~/

# 2. Sul server
zcat ~/db.sql.gz | vendor/bin/drush --root=web sql-cli
vendor/bin/drush --root=web updatedb -y
vendor/bin/drush --root=web config:import -y
vendor/bin/drush --root=web cache:rebuild
```

## Strategia SEO

Piano a fasi — tutte completate al 2026-05-28. Stato dettagliato in `memory/seo-plan.md`.

### Fasi completate

| Fase | Moduli | Stato |
|---|---|---|
| 1 — Fondamenta | metatag, simple_sitemap, redirect | ✅ 2026-05-23 |
| 2 — Structured data | schema_metatag, easy_breadcrumb | ✅ 2026-05-26 |
| 3 — E-E-A-T | breadcrumb HTML, Person/Organization schema | ✅ 2026-05-28 |
| 4 — Performance | lazy loading, fetchpriority, CSS/JS aggregation, image style corso_card | ✅ 2026-05-28 |
| 5 — Analisi in-CMS | real_time_seo | ⬜ opzionale |

### Sitemap (simple_sitemap)

- `commerce_product.corso` → `index: true`, priorità `1.0` — pagine pubbliche `/corsi/...`
- `node.corso` → `index: false` — riservato agli acquirenti
- `node.article` → priorità `0.8` | pagine generiche → `0.5`
- Lingua inglese rimossa (sito solo italiano)
- `Sitemap:` directive in `web/robots.txt`

Dopo ogni modifica alla sitemap: `drush simple-sitemap:generate`

### Analytics

- **Google Search Console** — verificato via DNS TXT record, sitemap inviata
- **Google Tag Manager** — `GTM-M4GQGW2J` via modulo `google_tag`
- **Google Analytics 4** — `G-N8PJG76Y0Y` configurato come tag GA4 in GTM

### Fase 5 — Analisi in-CMS (opzionale)
```bash
composer require drupal/real_time_seo
ddev drush en real_time_seo -y
```

## Block content custom — deploy in produzione

I `block_content` sono **contenuto** (database), non configurazione. La config `block.block.*.yml` referenzia il blocco tramite UUID: se quell'UUID non esiste nel DB di destinazione, il blocco appare come "broken or missing".

### Workflow corretto per blocchi custom

**Regola:** creare il blocco prima in **produzione**, poi portarlo in dev — mai il contrario.

1. Crea il blocco in prod via UI (`/block/add/<tipo>`)
2. Esporta la config in dev: `ddev drush cex -y`
3. In dev, ricrea il blocco con lo stesso UUID usando uno script drush:

```bash
ddev drush php:script scripts/create_<nome>_block.php
```

### Se il blocco esiste già in dev ma non in prod

Usa uno script PHP in `scripts/` da eseguire sul server:

```php
<?php
// scripts/create_<nome>_block.php
$uuid = '<uuid-dalla-config>';
$storage = \Drupal::entityTypeManager()->getStorage('block_content');
if ($storage->loadByProperties(['uuid' => $uuid])) {
  echo "Esiste già.\n"; return;
}
$block = $storage->create([
  'uuid'     => $uuid,
  'type'     => '<bundle>',
  'info'     => '<label>',
  'body'     => ['value' => '<html>', 'format' => 'full_html'],
  'status'   => TRUE,
  'langcode' => 'it',
]);
$block->save();
echo "Creato: {$block->id()}\n";
```

Sul server:
```bash
vendor/bin/drush --root=web --uri="https://lavorosicuramente.com" php:script scripts/create_<nome>_block.php
vendor/bin/drush --root=web --uri="https://lavorosicuramente.com" cache:rebuild
```

> Script di esempio già in `scripts/create_colonneservizi_block.php`.

## Comandi utili
```bash
# Compilare Tailwind (da web/themes/custom/tailwindcss/)
npm run build   # produzione, minified
npm run dev     # watch mode

# Pulire cache Drupal (sempre via DDEV)
ddev drush cr
# oppure: Admin → Configuration → Performance → Clear all caches
```
