# SEO_ACTION_PLAN.md — Alessandro Milani Liutaio

**Site:** alessandromilaniliutaio.com
**Audit date:** 2026-03-27
**Stack:** Drupal 11, PHP 8.3, Commerce 3.x, DDEV

---

## Priority 1 — Critical Fixes (Blocking Issues)

### P1.1 — Fix massively double-encoded HTML entities in node--38.html.twig

**Problem:** `web/themes/custom/woo_onepage/templates/node/node--38.html.twig` at line 127 contains:
```
&amp;amp;amp;amp;amp;amp;amp;lt;/body&amp;amp;amp;amp;amp;amp;amp;gt;
```
This is 7 levels of HTML entity encoding. Node 38 is the **front page** of the site. This broken HTML is visible to crawlers and may corrupt the page's source.

**Impact:** Renders malformed HTML to Googlebot; may suppress front page indexing signals.

**Fix:** Edit the twig template and remove or correctly render the problematic line. The raw value should be checked — it appears to be leftover debug/iframe embed code that got double-encoded repeatedly through Twig's auto-escaping.

```bash
# Inspect the template
code web/themes/custom/woo_onepage/templates/node/node--38.html.twig
# Line 127: delete or replace the malformed line
# Then clear cache
ddev drush cr
```

**Effort:** S (30 min)

---

### P1.2 — Complete front page metatag defaults

**Problem:** `metatag.metatag_defaults.front.yml` only sets `canonical_url` and `shortlink`. Missing: title, description, og:title, og:description, og:image, robots.

**Fix:** Export updated config after editing via UI or edit the YAML directly:

```yaml
# config/sync/metatag.metatag_defaults.front.yml — add:
tags:
  canonical_url: '[site:url]'
  shortlink: '[site:url]'
  title: 'Alessandro Milani Liutaio — Liutaio a Boca, Novara | [site:name]'
  description: 'Liutaio specializzato in archi da oltre 30 anni. Violini, viole e accessori artigianali. Bottega a Boca (NO). Shop online di accessori per strumenti ad arco.'
  robots: 'index, follow, max-image-preview:large'
  og_title: 'Alessandro Milani Liutaio — Artigiano degli Archi'
  og_description: 'Liutaio specializzato in violini e viole. Accessori artigianali di qualità. Shop online.'
  og_type: website
  og_site_name: alessandromilaniliutaio.com
  og_url: '[site:url]'
  og_image: '[site:url]/themes/custom/woo_onepage/images/og-homepage.jpg'
```

Then:
```bash
ddev drush cim
ddev drush cr
```

**Effort:** S (1 hour including writing copy)

---

### P1.3 — Verify node/ URLs are not publicly exposed

**Problem:** The system.site.yml stores front page as `/node/38` and 403 page as `/node/7`. Pathauto is configured but there is no guarantee all nodes have aliases. If any node lacks an alias, Drupal serves it at `node/NID` — which Google indexes as a separate URL.

**Verification:**
```bash
# Check nodes without aliases
ddev drush sql-query "SELECT n.nid, n.type, n.langcode FROM node_field_data n LEFT JOIN path_alias p ON p.path = CONCAT('/node/', n.nid) WHERE p.alias IS NULL AND n.status = 1;"
# Check front page alias
ddev drush sql-query "SELECT alias FROM path_alias WHERE path='/node/38';"
```

**Fix:** If any nodes lack aliases, regenerate:
```bash
ddev drush pathauto:aliases-generate node all
ddev drush pathauto:aliases-generate commerce_product all
```

Also add missing pathauto patterns for uncovered node types (see P3.1).

**Effort:** S (30 min verification, M if bulk aliases need regeneration)

---

### P1.4 — Audit hreflang implementation

**Problem:** Language negotiation uses URL prefix (`/it/` and `/en/`). simple_sitemap with `default_hreflang` is configured. However, some key checks are needed:

**Verification:**
```bash
# Check sitemap contains hreflang
curl https://alemilani10.ddev.site/sitemap.xml | grep -i hreflang | head -5
# Check a front page response for hreflang link tags
curl -s https://alemilani10.ddev.site/ | grep -i hreflang
```

**Fix if missing hreflang link tags in `<head>`:**
```bash
# Regenerate sitemap
ddev drush simple-sitemap:generate
```

Ensure metatag module has `hreflang` tags configured: check if `metatag_hreflang` submodule is needed (it may be provided by simple_sitemap's `<link rel="alternate">` injection).

**Effort:** S (1 hour to verify and fix)

---

## Priority 2 — Module Installation

### P2.1 — Install schema_metatag (JSON-LD structured data)

**Why:** Google heavily weights structured data for products, local businesses, and persons. Currently zero JSON-LD is emitted. This is a significant missed opportunity for rich snippets.

**Install:**
```bash
ddev composer require drupal/schema_metatag
ddev drush en schema_metatag schema_metatag_product schema_metatag_local_business schema_metatag_person schema_metatag_website
ddev drush cr
ddev drush cex
```

**Configure:**
- `LocalBusiness` schema on the front page / about page (address: Viale Artigiani 14, Boca, NO, Italy; phone: +393396378613; priceRange, openingHours)
- `Product` schema on all commerce product types (name, description, image, offers with price, currency, availability)
- `Person` schema for Alessandro Milani on about/portfolio pages
- `WebSite` schema with SearchAction on homepage

**Effort:** M (3–4 hours configuration across all content types)

---

### P2.2 — Install metatag_twitter_cards

**Why:** Twitter/X Cards allow link previews on social media. Missing despite OG tags being configured.

```bash
ddev composer require drupal/metatag
# metatag_twitter_cards is a submodule of drupal/metatag (already installed)
ddev drush en metatag_twitter_cards
ddev drush cr
ddev drush cex
```

Then add to metatag defaults for at least: node, front, commerce product types.

**Effort:** S (1 hour)

---

### P2.3 — Install imagemagick (optional but recommended)

**Why:** GD is limited. ImageMagick supports WebP generation, better JPEG quality control, and more efficient processing for large product images.

```bash
# First check if imagemagick is available in DDEV web container:
ddev exec "which convert"
# If available:
ddev composer require drupal/imagemagick
ddev drush en imagemagick
ddev drush cr
# Configure at: /admin/config/media/image-toolkit
```

**Note:** If ImageMagick is not in the DDEV container, add to `.ddev/config.yaml`:
```yaml
webimage_extra_packages: [imagemagick]
```
Then `ddev restart`.

**Effort:** S (1 hour)

---

### P2.4 — Install lazy_loader for global image lazy loading

**Why:** Images without `loading="lazy"` block rendering and hurt Core Web Vitals (LCP, FID).

```bash
ddev composer require drupal/lazy
ddev drush en lazy
ddev drush cr
ddev drush cex
# Configure at: /admin/config/media/lazy-loader
```

**Effort:** S (30 min)

---

## Priority 3 — Configuration

### P3.1 — Add pathauto patterns for missing content types

**Problem:** The following have no pathauto pattern and will fall back to `/node/NID` or `/product/NID`:
- Node types: `guida`, `portfolio`, `strumenti` (node), `webform`
- Commerce product types: `componenti`, `cordiera_cello`, `oggetti`, `servizi`, `servizi_riv`, `speciali`

**Fix (via UI or config YAML):**

For each missing node type, add a config file in `config/sync/`:

```yaml
# config/sync/pathauto.pattern.guida.yml
langcode: it
status: true
id: guida
label: 'Guide'
type: 'canonical_entities:node'
pattern: 'guida/[node:title]'
selection_criteria:
  entity_bundle:
    id: 'entity_bundle:node'
    negate: false
    bundles:
      guida: guida
weight: -5
```

```yaml
# config/sync/pathauto.pattern.portfolio.yml
langcode: it
status: true
id: portfolio
label: 'Portfolio'
type: 'canonical_entities:node'
pattern: 'portfolio/[node:title]'
selection_criteria:
  entity_bundle:
    id: 'entity_bundle:node'
    negate: false
    bundles:
      portfolio: portfolio
weight: -5
```

For commerce products, use the same structure as existing patterns (e.g., cordiera):

```yaml
# config/sync/pathauto.pattern.servizi.yml
langcode: it
status: true
id: servizi
label: 'Servizi'
type: 'canonical_entities:commerce_product'
pattern: '/servizi/[commerce_product:title]'
selection_criteria:
  entity_bundle:
    id: 'entity_bundle:commerce_product'
    negate: false
    bundles:
      servizi: servizi
weight: -5
```

After adding all configs:
```bash
ddev drush cim
ddev drush pathauto:aliases-generate node all
ddev drush pathauto:aliases-generate commerce_product all
ddev drush cr
```

**Effort:** M (2 hours — write YAMLs + regenerate aliases + verify)

---

### P3.2 — Add metatag defaults for all missing product types

**Problem:** 8 of 12 commerce product types have no specific metatag defaults (rely on global fallback only).

**Fix:** Create metatag defaults for each missing type. Use existing `metatag.metatag_defaults.commerce_product__cordiera.yml` as template.

Missing types: `accessori`, `componenti`, `cordiera_cello`, `oggetti`, `servizi`, `servizi_riv`, `speciali`, `strumenti`

Key tags to include for each:
```yaml
tags:
  title: '[commerce_product:title] | [site:name]'
  description: '[commerce_product:body:summary]'
  canonical_url: '[commerce_product:url:absolute]'
  og_title: '[commerce_product:title]'
  og_description: '[commerce_product:body:summary]'
  og_image: '[commerce_product:field_image:0]'
  og_type: product
  og_url: '[commerce_product:url:absolute]'
  og_site_name: alessandromilaniliutaio.com
  robots: 'index, follow'
  author: 'Alessandro Milani'
```

```bash
# After creating all YAML files:
ddev drush cim
ddev drush cr
```

**Effort:** M (2 hours)

---

### P3.3 — Add metatag defaults for missing node types

**Problem:** Node types `certificato`, `guida`, `portfolio`, `strumenti`, `webform` have no type-specific metatag defaults.

For each, create `metatag.metatag_defaults.node__{type}.yml` — use `metatag.metatag_defaults.node.yml` as base. Important type-specific settings:

- `portfolio`: `og_type: article`, image from portfolio image field
- `guida`: `og_type: article`, description from body summary
- `strumenti` (node): `og_type: product`, link to relevant shop page
- `webform`: `robots: noindex, nofollow` (contact forms should not be indexed)
- `certificato`: `robots: noindex, nofollow` (certificates are private/personal)

```bash
ddev drush cim
ddev drush cr
```

**Effort:** S–M (1–2 hours)

---

### P3.4 — Add all commerce product types to simple_sitemap

**Problem:** Product types `accessori`, `componenti`, `cordiera_cello`, `oggetti`, `servizi`, `servizi_riv`, `speciali` are absent from the sitemap.

**Fix:** Create sitemap bundle settings for each. Use existing `simple_sitemap.bundle_settings.default.commerce_product.cordiera.yml` as template:

```yaml
# config/sync/simple_sitemap.bundle_settings.default.commerce_product.accessori.yml
langcode: it
status: true
id: default.commerce_product.accessori
sitemap: default
entity_type: commerce_product
bundle: accessori
index: true
priority: '0.6'
changefreq: weekly
include_images: true
```

Repeat for each missing type. Then:
```bash
ddev drush cim
ddev drush simple-sitemap:generate
ddev drush cr
```

**Effort:** S (1 hour)

---

### P3.5 — Configure schema_metatag defaults (after P2.1)

**After installing schema_metatag**, configure JSON-LD for priority entities:

#### LocalBusiness (front page + contact page)
```
Type: LocalBusiness
Name: Alessandro Milani Liutaio
Address: Viale Artigiani 14, Boca, 28010, NO, Italy
Telephone: +393396378613
Email: shop@alessandromilaniliutaio.com
URL: https://alessandromilaniliutaio.com
Geo: 45.68328026608479, 8.411458883694884
```

#### Product (commerce product types)
```
Type: Product
Name: [commerce_product:title]
Description: [commerce_product:body:summary]
Image: [commerce_product:field_image:0]
Offers:
  Price: [commerce_product:variations:0:price:number]
  PriceCurrency: [commerce_product:variations:0:price:currency_code]
  Availability: InStock / OutOfStock based on availability field
```

#### Person (about/portfolio pages)
```
Type: Person
Name: Alessandro Milani
JobTitle: Liutaio
URL: https://alessandromilaniliutaio.com
```

**Effort:** L (4–6 hours — complex token configuration across multiple content types)

---

### P3.6 — Improve robots.txt for production

**Current:** Default Drupal robots.txt.

**Additions needed:**
```
# Block checkout and user paths from indexing
Disallow: /it/cart
Disallow: /en/cart
Disallow: /it/checkout
Disallow: /en/checkout
Disallow: /it/user
Disallow: /en/user
Disallow: /it/admin
Disallow: /en/admin
Disallow: /node/

# Point to sitemap
Sitemap: https://alessandromilaniliutaio.com/sitemap.xml
```

Edit `web/robots.txt` directly.

**Effort:** S (30 min)

---

### P3.7 — Review redirect module configuration

**Current:** Auto 301, passthrough_querystring: true, route_normalizer_enabled: true.

**Verify on production:**
- Confirm `www.alessandromilaniliutaio.com` → `alessandromilaniliutaio.com` redirect (httpswww module is enabled)
- Confirm `http://` → `https://` redirect
- Check for any redirect loops via: `curl -I https://www.alessandromilaniliutaio.com/`

**Effort:** S (30 min)

---

## Priority 4 — Content SEO

### P4.1 — Write meta descriptions for all key pages

**Problem:** Many pages rely on `[node:summary]` which may be empty if the body field has no summary defined.

**Fix:** For each important page, ensure the body field has a hand-written summary (150–160 characters, in both IT and EN). Key pages:
- Homepage
- About / Alessandro Milani page
- Each main product category
- Portfolio
- Contact/Services page

**Effort:** L (ongoing — copy writing)

---

### P4.2 — Optimize product page titles and descriptions

**Current:** Product titles follow pathauto pattern using `[commerce_product:title]`.

**Recommendations:**
- Titles should be: `{Product Name} — {Category} per {Instrument} | Alessandro Milani Liutaio`
- Descriptions should include: material, compatibility with violin/viola/cello, handcraft quality

Set via metatag token overrides or CKEditor summary field on each product.

**Effort:** L (ongoing content work)

---

### P4.3 — Add alt text to all product images

**Verify:**
```bash
# Check how many product images lack alt text
ddev drush sql-query "SELECT COUNT(*) FROM file_managed fm LEFT JOIN commerce_product__field_image pi ON fm.fid = pi.field_image_target_id WHERE pi.field_image_alt = '' OR pi.field_image_alt IS NULL;"
```

Alt text format: `{Product Name} — {Material} — Alessandro Milani Liutaio`

**Effort:** M (depends on number of products)

---

### P4.4 — Implement Open Graph images for all content types

**Problem:** Several metatag defaults reference `[node:field_image]` or `[commerce_product:field_image:0]` but if no image is set, og:image will be empty.

**Fix:** Add a fallback og:image in the global metatag defaults pointing to a generic branded image:
```yaml
# In metatag.metatag_defaults.global.yml add:
og_image: 'https://alessandromilaniliutaio.com/themes/custom/woo_onepage/images/og-default.jpg'
```

Create `og-default.jpg` (1200×630px) with the shop logo and luthier imagery.

**Effort:** S (1 hour including image creation)

---

### P4.5 — Blog content strategy

**Current:** `article` node type exists, pathauto pattern `blog/[node:title]` configured.

**Recommendations:**
- Publish monthly articles on: lutherie techniques, instrument maintenance, wood selection, accessory guides
- Target long-tail keywords: "cordiera per violino artigianale", "piroli ebano violino", "mentoniera violino misure"
- Each article should have: 800–1200 words, 1 header image with alt text, internal links to relevant products, bilingual (IT primary, EN translation)

**Effort:** L (ongoing — 4–8h/month)

---

## Priority 5 — Ongoing / Long-term

### P5.1 — Google Business Profile (GBP)

- Verify/claim GBP for "Alessandro Milani Liutaio" in Boca (NO)
- Ensure NAP is identical to site + schema: `Viale Artigiani 14, Boca, 28010, NO, Italy; +39 339 637 8613`
- Add photos of the bottega, instruments, accessories
- Collect reviews from customers
- Post monthly updates/photos

**Effort:** M initial setup; S monthly maintenance

---

### P5.2 — Google Search Console

- Verify both `https://alessandromilaniliutaio.com` and `https://www.alessandromilaniliutaio.com` in GSC
- Submit `https://alessandromilaniliutaio.com/sitemap.xml`
- Monitor Core Web Vitals (especially LCP for product image-heavy pages)
- Set target country: Italy (via international targeting if needed)

**Effort:** S (2 hours setup)

---

### P5.3 — Core Web Vitals optimization

**LCP (Largest Contentful Paint):**
- Preload hero images: `<link rel="preload" as="image" href="...">` in page--home.html.twig
- Ensure hero image uses responsive_image style (already configured)
- Consider WebP conversion (requires imagemagick — see P2.3)

**CLS (Cumulative Layout Shift):**
- Set explicit width/height on all `<img>` tags in templates
- Check product image containers for fixed aspect-ratio containers

**FID/INP:**
- React app pages (strumenti, prodotti) render client-side — verify TTI is acceptable
- Consider SSR or pre-rendering for key product listing pages

**Effort:** L (ongoing — monthly monitoring)

---

### P5.4 — Link building

- Italian lutherie forums and associations
- Music schools and conservatories in Piedmont
- Supplier links (wood, accessories distributors)
- Guest articles on instrument care blogs
- YouTube channel (instrument making/repair demos) linking back to site

**Effort:** L (ongoing)

---

### P5.5 — International SEO (IT/EN parity)

**Problem:** Italian is primary but English content may lag.

**Actions:**
- Audit English translations of all published products and pages
- Ensure `hreflang` alternate links appear correctly in `<head>` for both IT and EN versions
- Verify EN URLs are indexed (check GSC for `/en/` URL coverage)
- Consider adding German (DE) for Central European market (violinists, music schools)

**Effort:** M (2–4h audit) + L (translation work)

---

## Quick Win Summary (Do These First)

| Action | Effort | Impact |
|--------|--------|--------|
| P1.1 Fix double-encoded entities in node--38.html.twig | S | HIGH — front page rendering bug |
| P1.2 Complete front page metatag defaults | S | HIGH — front page OG/title missing |
| P2.1 Install schema_metatag | M | HIGH — rich snippets for products |
| P3.6 Improve robots.txt | S | MEDIUM — block checkout/admin from crawlers |
| P4.4 Add default og:image fallback | S | MEDIUM — social sharing quality |
| P3.4 Add missing product types to sitemap | S | MEDIUM — crawl coverage |
| P3.1 Add pathauto patterns for missing bundles | M | MEDIUM — clean URLs |
| P2.2 Enable metatag_twitter_cards | S | LOW-MEDIUM — social sharing |
