# International Expansion Technical Guide

**Last Updated:** 2026-03-16

## Overview

This document provides implementation details for Ordio's international expansion foundation (2026–2027). It covers file paths, hreflang patterns, locale config, waitlist API, and subdirectory routing.

---

## 1. File Structure

| Purpose | Path |
|---------|------|
| Locale config | `v2/config/locale-config.php` |
| Hreflang include | `v2/base/include_hreflang.php` |
| Head (hreflang) | `v2/base/head.php` |
| Waitlist API | `v2/api/waitlist-signup.php` |
| Waitlist form component | `v2/components/waitlist-form.php` |
| CTA buttons (waitlist conditional) | `v2/base/include_ctabuttons.php` |
| HubSpot config | `v2/config/hubspot-config.php` |
| GSC/GA4 by country script | `docs/strategy/international-expansion/scripts/fetch-international-traffic-data.php` |
| Target markets | `docs/strategy/international-expansion/TARGET_MARKETS.md` |

---

## 2. Locale Configuration

**File:** `v2/config/locale-config.php`

- **`$LOCALE_CONFIG`** – Defines supported locales with:
  - `code` – ISO language-region (e.g. de-DE, en-GB)
  - `hreflang` – Value for hreflang attribute
  - `hreflang_variants` – Optional array for DACH (de-DE, de-AT, de-CH)
  - `url_prefix` – Subdirectory ('' for default, 'en' for /en/)
  - `is_default` – Primary locale (x-default)
  - `is_pre_launch` – If true, show waitlist instead of demo CTAs

**Functions:**
- `get_active_locales()` – Returns all active locales
- `get_default_locale_code()` – Returns default hreflang
- `get_locale_url($localeKey, $path)` – Builds full URL for locale
- `is_pre_launch_market($localeKey)` – Returns true for pre-launch locales

**Adding a new locale:** Uncomment or add entry in `$LOCALE_CONFIG`, set `is_pre_launch: true` for waitlist behavior.

---

## 3. Hreflang Implementation

**File:** `v2/base/include_hreflang.php`

- Included from `v2/base/head.php`
- Outputs `<link rel="alternate" hreflang="..." href="...">` for each locale
- Uses `get_locale_url()` for alternate URLs
- Outputs `x-default` pointing to default locale

**Current state:** Single locale (de) – outputs de-DE, de-AT, de-CH, x-default (all same URL).

**When adding locales:** Ensure reciprocal hreflang links; validate with Google Search Console International Targeting.

### Hreflang Conflict Resolution

**Common Issues and Fixes:**

1. **"No self-referencing hreflang" errors**
   - **Cause:** Google crawls URLs with query parameters or trailing slashes that don't match canonical URLs
   - **Solution:** Redirect problematic URLs to canonical versions in `.htaccess`
   - **Example:** `/demo?page=abwesenheiten` → `/demo` (301 redirect)

2. **Query parameter URLs**
   - **Problem:** URLs like `/demo?page=abwesenheiten` create duplicate content and hreflang conflicts
   - **Fix:** Redirect query parameter URLs to clean canonical URLs
   - **Implementation:** `.htaccess` rules strip `page=` parameter from `/demo` URLs

3. **Trailing slash mismatches**
   - **Problem:** Canonical URLs don't have trailing slashes, but Google crawls versions with trailing slashes
   - **Fix:** Redirect trailing slash versions to non-trailing slash versions
   - **Example:** `/insights/dienstplan/` → `/insights/dienstplan` (301 redirect)

**Best Practices:**

- **Always use canonical URLs for hreflang self-reference** – `include_hreflang.php` uses `$canonical_url` if set
- **Redirect before serving** – Use `.htaccess` redirects to normalize URLs before PHP processes them
- **Never include query parameters in hreflang tags** – Query parameters should never appear in hreflang URLs
- **Match canonical URLs exactly** – Hreflang self-reference must match canonical URL exactly
- **Validate in GSC** – Check Google Search Console International Targeting for errors after changes

**Redirect Rules (`.htaccess`):**

```apache
# Redirect demo query parameters to clean /demo URL (fixes hreflang conflicts)
# Query parameters like ?page=abwesenheiten create duplicate content and hreflang issues
# Canonical URL is /demo (without query params), so redirect to match canonical
# QSD flag (Query String Discard) strips query parameters from redirect target
RewriteCond %{QUERY_STRING} ^page= [NC]
RewriteCond %{REQUEST_URI} ^/demo/?$ [NC]
RewriteRule ^demo/?$ /demo [R=301,L,QSD]

# Redirect pillar pages with trailing slash to canonical (no trailing slash)
# This fixes "No self-referencing hreflang" errors for pillar pages
RewriteRule ^insights/dienstplan/?$ /insights/dienstplan [R=301,L]
RewriteRule ^insights/zeiterfassung/?$ /insights/zeiterfassung [R=301,L]
```

**Key Changes (2026-03-16):**

- **Demo redirect:** Added `QSD` flag to properly strip query parameters (Apache 2.4+)
- **Pillar redirects:** Simplified rules to directly match and redirect trailing slash versions
- **Placement:** Rules placed before blog post routing to ensure they execute first

---

## 4. Subdirectory Routing (Future)

**Recommended structure:** `ordio.com/en/`, `ordio.com/nl/`

- Not yet implemented; intended for Phase 2 localization
- `.htaccess` or web server config will route `/en/*` to appropriate handlers
- Canonical URLs: Use locale-prefixed path for localized pages

---

## 5. Waitlist System

### API: `v2/api/waitlist-signup.php`

- **Method:** POST
- **Input (JSON):** `email` (required), `country`, `company_size`, `page_url`
- **Output:** `{ success: bool, message: string }`
- **HubSpot:** Creates contact via CRM API with properties: email, country, leadsource, lifecyclestage, hs_lead_status, description
- **Read-only endpoint:** Added to `hubspot-config.php` for fallback token when env not set

### Component: `v2/components/waitlist-form.php`

- Email + country dropdown
- Submits to `/v2/api/waitlist-signup.php`
- Configurable `$waitlistHeadline` and `$waitlistDescription`

### CTA Integration: `v2/base/include_ctabuttons.php`

- When `is_pre_launch_market()` is true: shows waitlist form instead of demo buttons
- Callback button hidden for pre-launch
- Requires `$locale` or `$GLOBALS['locale']` to be set for non-default locales

---

## 6. Data Collection Script

**File:** `docs/strategy/international-expansion/scripts/fetch-international-traffic-data.php`

**Usage:**
```bash
php docs/strategy/international-expansion/scripts/fetch-international-traffic-data.php [options]
```

**Options:**
- `--gsc-only` – GSC data only
- `--ga4-only` – GA4 data only
- `--days=N` – Days to analyze (default: 90)
- `--dry-run` – No API calls

**Output:**
- `docs/strategy/international-expansion/data/gsc-by-country.json`
- `docs/strategy/international-expansion/data/ga4-by-country.json`

**Dependencies:** Google API credentials, Composer

---

## 7. Schema and Meta

- **`inLanguage`:** Update schema per locale when localizing (e.g. `en-GB`, `nl-NL`)
- **Meta:** Set `lang` attribute on `<html>` per locale
- **Canonical:** Use locale-prefixed URL for localized pages

---

## 8. Validation Checklist

- [ ] Hreflang: Reciprocal links, valid ISO codes, x-default
- [ ] Waitlist: Test signup flow, verify HubSpot contact creation
- [ ] Locale config: `is_pre_launch` correctly set for new locales
- [ ] GSC International Targeting: No errors after adding locales

---

## References

- [International Expansion Plan](../../.cursor/plans/ordio_international_expansion_plan_a67081e3.plan.md)
- [TARGET_MARKETS.md](TARGET_MARKETS.md)
- [BLOG_CONTENT_STRATEGY.md](BLOG_CONTENT_STRATEGY.md)
- [TRAFFIC_AND_BRAND_BUILDING.md](TRAFFIC_AND_BRAND_BUILDING.md)
