{"id":"6939c5bfab8ed1a68f99cd8e","title":"2025 12 10","path":"how-to/technical-articles/2025-12-10","contentMarkdown":"# Content Migration Guide\n\n## Overview\n\nThe Content Migration system provides a template-based workflow for migrating pages from external websites into your NGX-Ramblers site structure. Instead of hard-coded parsing logic or abstract transformation steps, you:\n\n1. **Build a visual template** using the page editor\n2. **Configure data mappings** for each element (where content comes from)\n3. **Apply the template** to migrate multiple pages automatically\n\n### Key Features\n\n- **Visual template creation**: What you build is what migrated pages will look like\n- **Declarative mappings**: Configure extraction rules without writing code\n- **Dynamic content generation**: Automatically create nested rows from scraped content\n- **Reusable templates**: One template can migrate hundreds of pages\n- **Parent page support**: Create index pages with action buttons\n- **Pattern matching**: Extract specific content using text/image patterns\n\n### Template Types\n\n- **Shared Fragment**: Reusable components (headers, footers, sidebars) inserted into multiple pages\n- **User Template**: Custom page layouts for quickly building new pages with consistent structure\n- **Migration Template**: Templates with data mappings for migrating content from external sources\n\n## Quick Start\n\n### 1. Create a Migration Template\n\n1. Navigate to a page in site edit mode (e.g., `/routes/example-route`)\n2. Build your ideal structure using the page editor:\n   - Add rows (Text, Location, Map, Index)\n   - Arrange them exactly as you want migrated pages to look\n   - Configure row settings (height, columns, visibility)\n3. Enable **Template Mode**:\n   - Toggle \"Template options\" switch (top right)\n   - Click the **\"Migration template\"** badge button\n4. Enable **Mapping Mode**:\n   - Toggle **\"Mapping mode\"** switch\n   - This reveals mapping configuration UI for each row\n5. Configure mappings (see sections below)\n6. Click **\"Create template\"** to publish\n\n### 2. Configure Parent Pages\n\nNavigate to **Admin → System Settings → Migration → Settings tab**\n\n1. Add a parent page for your migration\n2. Configure the settings (see Parent Page Configuration section)\n3. Select your migration template from the \"Override Template\" dropdown\n4. Save configuration\n\n### 3. Run Migration\n\n1. Stay in Migration Settings\n2. Configure which pages to migrate\n3. Click **Run migration**\n4. Monitor progress in the Activity tab\n\n## Parent Page Configuration\n\nEach migration site can have multiple **Parent Pages** - these are index/category pages that contain lists of child pages to migrate.\n\n### Parent Page Settings\n\nFor each parent page, configure:\n\n**URL Pattern**: The parent page URL\n- Example: `/walks/routes`\n- The source page containing links to child pages\n\n**Path Prefix**: Prefix for child page paths\n- Example: `walks/routes/`\n- Used to construct destination paths for migrated pages\n\n**Link Selector**: CSS selector for child page links (optional)\n- Defaults to content area if empty\n- Example: `.route-list a` to target specific links\n\n**Max Child Pages**: Limit number of children to migrate (optional)\n- Example: `50` to migrate only first 50 pages\n- Leave empty to migrate all found pages\n\n### Migrate Parent Page Options\n\nControls what happens to the parent page itself during migration:\n\n#### Not migrated (default)\n\n- The parent page **itself is skipped**\n- Child pages **are still migrated**\n- Use when: Parent page doesn't exist or you'll create it manually\n- Example: Skip `/walks/routes` but migrate `/walks/routes/wealdway`\n\n#### As-is\n\n- Parent page **is migrated** with its original content\n- Content extracted and migrated like any other page\n- Use when: Parent page has intro text or useful content to preserve\n- Example: `/walks/routes` has introductory text about routes\n\n#### With Links as Action Buttons Row\n\n- Parent page **is migrated** with child page links converted to action buttons\n- Creates an **auto-generated index page**\n- Use when: You want a navigation/directory page\n- Example: `/walks/routes` becomes a page with [Weald Way] [North Downs Way] [Thames Path] buttons\n\n### Override Template (optional)\n\nBy default, child pages use the site's default migration template. Use this field to specify a **different template** for children under this parent page.\n\n**Use cases:**\n- Different page structures for different sections\n  - Routes use \"routes-template\"\n  - Regional routes use \"regional-routes-template\"\n- Special layouts for specific categories\n  - Index pages with summaries\n  - Detail pages with maps\n\n**How it works:**\n- Select a template from the dropdown\n- All child pages under this parent use the selected template\n- If empty, child pages use the site's default template\n- Template selection shows all available migration templates from your `fragments/templates/` folder\n\n**Example configuration:**\n```\nParent Page: /walks/routes\n├─ Migrate parent page: With Links as Action Buttons Row\n├─ Override Template: routes-template\n└─ Max child pages: 50\n\nResult:\n- /walks/routes → Index page with action buttons\n- /walks/routes/wealdway → Uses routes-template\n- /walks/routes/north-downs-way → Uses routes-template\n```\n\n## Template Creation Workflow\n\n### UI Components\n\n#### Template Options Panel\n\nLocated at top of page editor when \"Template options\" toggle is ON:\n\n```\n[Shared fragment][User template][Migration template] [Remove] [Create template] [Mapping mode ☑]\n```\n\n- **Type badges**: Select template type\n- **Remove**: Clear template configuration\n- **Create template**: Publish to fragment library\n- **Mapping mode**: Toggle migration mapping UI (migration templates only)\n\n#### Template Library Panel\n\nShows published templates for reuse:\n\n```\nTemplate library\n├─ Dropdown (shows fragment paths)\n└─ [Replace] [Append]\n```\n\n- **Replace**: Replace current page content with template\n- **Append**: Append template to current page\n\n#### Paste Page Content\n\nThe page editor includes a **Paste page content** badge next to save/revert buttons. Use this to:\n\n- Clone complex templates between environments\n- Try out example content (like `docs/routes-template-page-content.json`)\n- Restore earlier snapshots from API explorer\n\nThe parser automatically unwraps API response shapes, so you can paste output from `/api/page-content?path=...` directly.\n\n## Data Mapping Configuration\n\n### Row-Level Mappings\n\nWhen Mapping Mode is enabled, each row shows configuration based on its type.\n\n#### Location Row Mapping\n\n```\nSource type: [Extract] [Static] [Metadata]\n☑ Extract from content\n☑ Hide location row\nDefault location: [____________]\n```\n\n**Extract from content**: Automatically extract location from narrative text\n- Looks for postcodes, grid references, place names\n- Geocodes to latitude/longitude\n\n**Hide location row**: Make the row invisible\n- Row data still available for Map rows via `useLocationFromRow`\n- Use when location is only for map positioning, not display\n\n**Default location**: Fallback location if extraction fails\n- Example: \"Tonbridge, Kent\" for Kent-based routes\n- Ensures map always has a center point\n\n#### Map Row Mapping\n\n```\n☑ Extract GPX from content\nGPX file path: [____________]\nUse location from row: [0]\n```\n\n**Extract GPX from content**: Pull GPX file from source page\n- Searches for .gpx file links in source HTML\n- Downloads and processes route data\n\n**GPX file path**: Static GPX file path (alternative to extraction)\n- Example: `/assets/routes/wealdway.gpx`\n\n**Use location from row**: Row index to get location coordinates from\n- Example: `0` references first row (Location row)\n- Map centers on this location\n- Supports nested row references (e.g., `0.1` = row 0, nested row 1)\n\n#### Metadata Row Mapping\n\n```\nSource type: [Metadata ▼]\nField: [Title | Path | Migration note]\nPrefix: [Automatically migrated from]\nDate format: [yyyy-LL-dd HH:mm]\n```\n\n**Field options:**\n- `Title`: Page title from scraped metadata\n- `Path`: Source page URL/path\n- `Migration note`: \"Migrated from [URL] on [date]\" footer\n\n**Customization:**\n- `metadataPrefix`: Custom prefix text (default: \"Migrated from\")\n- `metadataDateFormat`: Luxon format string (default: \"yyyy-LL-dd HH:mm\")\n\n#### Index Row Mapping\n\nIndex rows use existing Index row settings:\n- Content types (walks, routes, social events)\n- Render modes (carousel, grid, list)\n- Map settings (clustering, markers)\n\nNo additional mapping configuration required.\n\n### Column-Level Mappings\n\nFor rows with columns containing nested rows, column-level mapping configuration appears.\n\n#### Column Mapping UI\n\n```\nColumn 2 Mapping\n─────────────────────────────────────\nSource Type: [Extract | Static | Not mapped]\nContent Type: [Text | Image | Mixed]\n\nNested Rows Configuration\n─────────────────────────────────────\nContent source: [Extract from content ▼]\nPacking behavior: [One row per item ▼]\n☑ Group text with images\n\nDocumentation notes\n[Notes about what this mapping does...]\n```\n\n#### Content Source Options\n\n**Not mapped**\n- Column stays exactly as designed in template\n- Use for static content (headers, footers, shared fragments)\n\n**Extract from content**\n- Populate from scraped page data\n- Requires Content Type and packing configuration\n\n**Static content**\n- Use template content as-is\n- Allows adding documentation notes\n\n#### Content Type (what to extract)\n\n**Remaining images**\n- Images not yet consumed by other mappings\n- Recommended for galleries to avoid duplication\n- Each column gets unique images\n\n**Remaining text**\n- Text segments (paragraphs, headings) still unused\n- Recommended catch-all for narrative text\n- Prevents double-rendering content\n\n**All content**\n- Every text and image segment in source order\n- Use when column should mirror original story exactly\n\n**All images**\n- Every image, even if already used elsewhere\n- Use when multiple galleries need same images\n\n**Pattern match**\n- Only segments matching custom pattern\n- Supports regex, filename glob, CSS selector\n- Pattern input appears when selected\n\n#### Packing Behavior (when extracting)\n\n**One row per item**\n- Create separate nested row for each extracted item\n- Example: One nested row per image\n- Repeats until no matches remain\n\n**All in one row**\n- Put all extracted content in single nested row\n- Example: All narrative text in one row\n- Produces single combined row\n\n**Collect with breaks**\n- Group content based on document structure\n- Smart breaking on headings, images, paragraphs\n- Configurable break points\n\n#### Additional Options\n\n**Group text with images** (`groupTextWithImage`)\n- Associates caption text with each image\n- Looks for text immediately following image\n- Critical for preserving image captions\n\n**Hide row if empty** (`hideIfEmpty`)\n- Removes entire row when no content extracted\n- Prevents blank placeholders\n- Useful for optional sections\n\n**Documentation notes**\n- Explain what this mapping does\n- Visible in mapping mode\n- Example: \"Points-of-interest sidebar with remaining images\"\n\n### Dynamic Target Rows\n\nWhen Mapping mode is enabled, you can mark any nested row as a **Dynamic target** using the badge button in the nested row header (stacked-layers icon).\n\n**What it does:**\n- Marks which nested row will be cloned and populated with extracted content\n- The selected row becomes the template for dynamically generated rows\n- Only one nested row can be the dynamic target per column\n\n**How it works:**\n1. Content Source specifies **what** to extract and **how** to pack it\n2. Dynamic target button specifies **which row structure** to use\n3. When migration runs:\n   - Extracts content based on Content Type setting\n   - Clones the target nested row\n   - Fills clone with extracted content\n   - Repeats if packing behavior is \"one-per-item\"\n4. Other nested rows (without button) are static and added after dynamic rows\n\n**Example:**\n\n```\nNested Rows Configuration:\n├─ Content source: remaining-images\n├─ Packing: one-per-item\n│\n├─ Nested Row 1 (Text) ← \"Use as dynamic target\" (active)\n│  └─ Column: image + caption placeholder\n│\n└─ Nested Row 2 (Shared Fragment) ← Static row\n    └─ Shared fragment reference\n```\n\n**Result:**\n- Nested row 1 cloned for each remaining image\n- Each clone filled with image + caption\n- Nested row 2 (shared fragment) added once at end\n\n### Column Processing Order\n\n**Important:** When a row has multiple columns with dynamic collection, order matters:\n\n1. **Columns with `remaining-images`** processed **first**\n   - Allows images to capture their caption text\n2. **Columns with `remaining-text`** processed **second**\n   - Gets narrative text after images claimed captions\n\nThis ensures images grab caption text before remaining-text column consumes all text.\n\n**Implementation:** The system automatically sorts column mappings by priority:\n1. Pattern-match images (specific content)\n2. Remaining-images with `groupTextWithImage: true`\n3. Other content types\n4. Remaining-images without text grouping\n\n## Content Matchers & Patterns\n\n### Text Patterns\n\nUsed when Content Type is `TEXT`:\n\n**ALL_TEXT_UNTIL_IMAGE**\n- Extract all text from current position until first image\n- Commonly used for intro paragraphs\n- Stops at first image boundary\n\n**ALL_TEXT_AFTER_HEADING**\n- Extract heading and following text until next heading or image\n- Good for section extraction\n- Captures semantic blocks\n\n**REMAINING_TEXT**\n- Extract all remaining unused text content\n- Use at end of transformation to capture everything left\n- Ensures no content is lost\n\n**PARAGRAPH**\n- Extract single paragraph or text segment\n- Advances to next segment on each use\n- For granular text extraction\n\n**STARTS_WITH_HEADING**\n- Extract text beginning with markdown heading (# through ######)\n- Filters to heading-led sections\n- Useful for structured content\n\n**CUSTOM_REGEX**\n- Match text using custom regular expression\n- Requires `customRegex` field\n- Powerful for complex extraction patterns\n- Test patterns using regex101.com\n\n### Image Patterns\n\nUsed when Content Type is `IMAGE`:\n\n**FIRST_IMAGE**\n- Extract first unused image\n- Most commonly used pattern\n- Marks image as used\n\n**REMAINING_IMAGES**\n- Extract ALL remaining unused images\n- Marks all as used\n- Use when consuming all remaining images\n\n**ALL_IMAGES**\n- Extract ALL images regardless of used status\n- Does NOT mark as used\n- Useful for showing images in multiple places (carousel + gallery)\n\n**FILENAME_PATTERN**\n- Match images by filename using wildcards\n- Requires `filenamePattern` field\n- Example: `*route-map*.jpg`\n- Case-sensitive matching\n\n**ALT_TEXT_PATTERN**\n- Match images by alt text using regex\n- Requires `altTextPattern` field\n- Example: `^Map of.*`\n- Useful for semantic image selection\n\n### Pattern Usage Table\n\n| Pattern | Use Case | Marks as Used? |\n|---------|----------|----------------|\n| `FIRST_IMAGE` | Single hero image, featured image | Yes |\n| `REMAINING_IMAGES` | Gallery consuming all remaining | Yes |\n| `ALL_IMAGES` | Show all images in carousel, then detail | No |\n| `FILENAME_PATTERN` | Specific image like route map | Yes |\n| `ALT_TEXT_PATTERN` | Images with specific alt text | Yes |\n\n### Understanding \"Used\" vs \"Unused\" Content\n\nThe transformation engine tracks which content has been consumed:\n\n**Text Content:**\n- Starts with all markdown text segments marked as \"unused\"\n- When text pattern matches and extracts content, segments marked as \"used\"\n- `REMAINING_TEXT` extracts all segments still marked \"unused\"\n\n**Image Content:**\n- All scraped images start as \"unused\"\n- `FIRST_IMAGE` extracts first unused image and marks it used\n- `REMAINING_IMAGES` extracts ALL unused images and marks all used\n- `ALL_IMAGES` extracts ALL images but does NOT mark them used (allows reuse)\n\n### Best Practices for Content Extraction\n\n**1. Extract in Order of Specificity**\n```\nStep 1: Extract specific content (e.g., route map by filename)\nStep 2: Extract structured content (e.g., intro text until image)\nStep 3: Extract remaining content (e.g., remaining-text, remaining-images)\n```\n\n**2. Use REMAINING Patterns Last**\nAlways use `REMAINING_TEXT` or `REMAINING_IMAGES` in final rows to ensure no content is lost.\n\n**3. Test Custom Regex Incrementally**\nStart simple and add complexity:\n- Test: `^###` (just headings)\n- Then: `^###\\\\s+.+` (heading with text)\n- Finally: `^###\\\\s+.+[\\\\s\\\\S]*?(?=###|$)` (heading with all text until next heading)\n\n**4. Filename Patterns Are Case-Sensitive**\n- Use lowercase patterns: `*route-map*` not `*Route-Map*`\n- Check actual filenames in Activity log output\n- Wildcards: `*` matches any characters\n\n**5. Extract Images Before Text**\nWhen both use `remaining-*` patterns, ensure images process first to capture captions before text consumes them.\n\n## Common Template Patterns\n\n### Pattern: Two-Column Layout with Featured Image\n\n```\nRow 1: Full-width intro text\nRow 2: Two columns\n  ├─ Column 1 (8 cols): Featured image\n  └─ Column 2 (4 cols): Remaining content in nested rows\n```\n\n**Mapping:**\n- Row 1, Column 1: Text pattern = `ALL_TEXT_UNTIL_IMAGE`\n- Row 2, Column 1: Image pattern = `FIRST_IMAGE`\n- Row 2, Column 2: Content source = `remaining-text`, Packing = `all-in-one`\n\n### Pattern: Route Page with Hidden Location\n\n```\nRow 1: Location (hidden)\nRow 2: Map (references Row 1)\nRow 3: Two columns (narrative + sidebar)\n  ├─ Column 1 (8 cols): Main narrative text\n  └─ Column 2 (4 cols): Points of interest images\n```\n\n**Mapping:**\n- Row 1: Extract from content ☑, Hide row ☑\n- Row 2: Use location from row = `0`\n- Row 3, Column 1: Content source = `remaining-text`, Packing = `all-in-one`\n- Row 3, Column 2: Content source = `remaining-images`, Packing = `one-per-item`, Group text with images ☑\n\n### Pattern: Photo Gallery with Map\n\n```\nRow 1: Two columns\n  ├─ Column 1 (6 cols): Map showing photo locations\n  └─ Column 2 (6 cols): Dynamic gallery (one row per photo)\n```\n\n**Mapping:**\n- Row 1, Column 1: Static (map configured in template)\n- Row 1, Column 2: Content source = `remaining-images`, Packing = `one-per-item`\n\n### Pattern: Index Page with Action Buttons\n\n```\nRow 1: Intro text\nRow 2: Index row (maps + clustering)\nRow 3: Action buttons for child pages\n```\n\n**Parent Page:**\n- Migrate parent page: `With Links as Action Buttons Row`\n- This automatically creates Row 3 with buttons\n\n## Advanced Topics\n\n### Nested Row Mappings Data Model\n\n```typescript\ninterface NestedRowMappingConfig {\n  contentSource: 'remaining-images' | 'remaining-text' | 'all-content' | 'all-images' | 'pattern-match';\n  packingBehavior: 'one-per-item' | 'all-in-one' | 'collect-with-breaks';\n  breakOn?: 'image' | 'heading' | 'paragraph';\n  stopOn?: 'image' | 'heading';\n  groupTextWithImage?: boolean;\n  filenamePattern?: string;\n  textPattern?: string;\n  imagePattern?: string;\n}\n```\n\n### Column-Level Mappings Data Model\n\n```typescript\ninterface ColumnMappingConfig {\n  columnIndex: number;\n  sourceType?: 'extract' | 'static' | 'metadata';\n  contentType?: 'text' | 'image' | 'mixed';\n  extractPreset?: string;\n  extractPattern?: string;\n  nestedRowMapping?: NestedRowMappingConfig;\n  notes?: string;\n}\n```\n\n### Row-Level Mappings Data Model\n\n```typescript\ninterface MigrationTemplateMapping {\n  targetRowIndex: number;\n  sourceType?: 'extract' | 'static' | 'metadata';\n\n  // Type-specific mappings\n  location?: MigrationTemplateLocationMapping;\n  map?: MigrationTemplateMapMapping;\n  index?: MigrationTemplateIndexMapping;\n\n  columnMappings?: ColumnMappingConfig[];\n  hideIfEmpty?: boolean;\n  notes?: string;\n}\n```\n\n### Bootstrap Grid System\n\nRemember the 12-column grid:\n- Full width: 12\n- Half width: 6\n- Two-thirds: 8, one-third: 4\n- Three columns: 4, 4, 4\n\nColumns in a row should sum to 12 for best results.\n\n### Image Styling\n\nImages support:\n- `imageBorderRadius`: Border radius in pixels (default: 6)\n- `alt`: Alt text (auto-populated from scraped alt)\n- `imageSource`: URL to image file\n\n### Content Exclusions\n\nBefore transformation, filter content using site config:\n- `excludeSelectors`: Remove HTML elements before scraping\n- `excludeTextPatterns`: Remove text matching regex\n- `excludeMarkdownBlocks`: Remove exact markdown blocks\n- `excludeImageUrls`: Don't use specific images\n\n## Troubleshooting\n\n### Template Issues\n\n**Template not appearing in library**\n- Check template was published (click \"Create template\")\n- Verify \"Template options\" toggle is ON\n- Check fragment path doesn't conflict with existing content\n\n**Mapping mode not showing**\n- Ensure \"Migration template\" type is selected\n- Toggle \"Mapping mode\" switch\n\n### Content Extraction Issues\n\n**Content not appearing**\n- Check Activity log for matcher debug output\n- Adjust pattern or use \"all\" to see all content first\n- Verify content source (remaining vs all)\n\n**Wrong image selected**\n- Filename pattern too broad or too narrow\n- Check image filenames in scraped content\n- Use more specific patterns\n\n**Images show placeholder text instead of captions**\n- Set `groupTextWithImage: true` in nested row mapping\n- Ensure images processed before remaining-text\n\n**Narrative column includes image caption headings**\n- System auto-sorts to process images first\n- Verify column processing order in mappings\n\n**Nested rows not populating**\n- Check targetRow and targetColumn index (zero-based)\n- Verify dynamic target row is marked\n- Check content source pattern matches content\n\n**Text split incorrectly**\n- Try different text patterns\n- `ALL_TEXT_UNTIL_IMAGE` stops at first image\n- `REMAINING_TEXT` takes everything unused\n\n**Custom regex not matching**\n- Test regex using regex101.com\n- Escape special characters\n- Use `[\\\\s\\\\S]*?` for multiline matching\n- Common patterns:\n  - Match heading + text: `^###\\\\s+.+[\\\\s\\\\S]*?(?=###|$)`\n  - Match paragraph: `^[^#\\\\n].+(?:\\\\n[^#\\\\n].+)*`\n\n**Filename pattern not matching images**\n- Check actual filenames in scraped content\n- Use wildcards correctly: `*route*` matches any file containing \"route\"\n- Case-sensitive by default\n- Patterns match against full URL path, not just filename\n\n**No nested rows generated**\n- Content type pattern didn't match anything\n- Check debug logs in Activity tab\n- Verify content source (e.g., \"remaining-images\" vs \"all-images\")\n\n**All content in one nested row when expecting multiple**\n- Packing behavior set to \"all-in-one\"\n- Change to \"one-per-item\" for separate rows\n\n### Location & Map Issues\n\n**Location extraction not working**\n- Verify narrative contains postcode or grid reference\n- Check extraction logs in browser console\n- Use default location as fallback\n\n**Map not centering on location**\n- Check `useLocationFromRow` index is correct\n- Verify location row has valid coordinates\n- Review location extraction success in logs\n\n**Blank maps showing**\n- No valid location data extracted\n- Maps now automatically hidden when no coordinates\n- Check location row configuration\n\n## Example Workflows\n\n### Example 1: Migrating Route Pages\n\n**Goal:** Migrate old route pages with location extraction, map display, and narrative content.\n\n**Template Structure:**\n```\nRow 0: Location (hidden, extracted from content)\nRow 1: Map (500px height, uses location from Row 0)\nRow 2: Two columns\n  ├─ Column 1 (8 cols): Main narrative text\n  └─ Column 2 (4 cols): Points of interest images with captions\nRow 3: Migration note (metadata)\n```\n\n**Mappings:**\n1. Row 0 (Location):\n   - Extract from content: ☑\n   - Hide row: ☑\n   - Default location: \"Kent, England\"\n\n2. Row 1 (Map):\n   - Use location from row: `0`\n   - Auto-fit bounds: ☑\n\n3. Row 2, Column 1:\n   - Source type: Extract\n   - Content type: Text\n   - Nested rows: Content source = `remaining-text`, Packing = `all-in-one`\n\n4. Row 2, Column 2:\n   - Source type: Extract\n   - Content type: Mixed\n   - Nested rows: Content source = `remaining-images`, Packing = `one-per-item`, Group text ☑\n   - Notes: \"Points-of-interest sidebar with image captions\"\n\n5. Row 3 (Metadata):\n   - Source type: Metadata\n   - Field: Migration note\n   - Prefix: \"Migrated from\"\n\n**Parent Page Config:**\n- URL: `/walks/routes`\n- Migrate parent page: `With Links as Action Buttons Row`\n- Override template: `routes-template`\n- Max children: 100\n\n**Result:** All route pages get consistent structure with extracted locations, centered maps, narrative text, and image galleries with preserved captions.\n\n### Example 2: Photo Gallery Migration\n\n**Goal:** Complex gallery with map showing photo locations and dynamic nested rows.\n\n**Template Structure:**\n```\nRow 0: Title and description\nRow 1: Two columns\n  ├─ Column 1 (6 cols):\n  │   ├─ Nested Row 0: Description text\n  │   ├─ Nested Row 1: Location (hidden)\n  │   └─ Nested Row 2: Map (references nested row 1)\n  └─ Column 2 (6 cols): Dynamic gallery rows (one per photo)\n```\n\n**Mappings:**\n1. Row 1, Column 1:\n   - Source type: Static\n   - Nested rows configured in template\n\n2. Row 1, Column 2:\n   - Source type: Extract\n   - Content source: `remaining-images`\n   - Packing: `one-per-item`\n   - Mark Nested Row 0 as dynamic target\n\n**Result:** Gallery page with map on left, dynamically generated photo rows on right (one row per image).\n\n## Implementation Status\n\n### ✅ Completed\n\n- Template type system (Shared/User/Migration)\n- Template UI with badge button groups\n- Template publishing to fragment library\n- Location row mapping UI\n- Map row mapping UI\n- Metadata row mapping UI\n- Column-level mapping UI for nested rows\n  - Content source selection\n  - Packing behavior configuration\n  - Content type selection\n  - Pattern matching support\n- Dynamic target row selection\n- Template descriptions\n- Paste page content functionality\n- Basic template transformation engine (`transformWithTemplate`)\n  - Static row cloning\n  - Metadata population\n  - Location/Map row handling\n- Column mapping data model\n- Migration runtime integration\n  - Site-level template selection\n  - Parent page template override\n  - Template priority over legacy transformations\n- Dynamic nested row extraction\n  - Column mappings drive row generation\n  - Shared fragments preserved\n  - `hideIfEmpty` support\n- Column processing order optimization\n  - Images processed before text\n  - Caption preservation for image galleries\n\n### 🚧 In Progress\n\n- GPX file extraction from content\n- Advanced pattern matching for text extraction\n- Enhanced location narrative parsing\n\n### 📋 Planned\n\n- Template preview in migration settings\n- Batch migration with templates\n- Template validation\n- Visual mapping UI with drag-and-drop\n- Template marketplace for sharing\n- Migration preview/diff view\n- Rollback capability\n- Template versioning\n- Real-time migration progress tracking\n- Error recovery and retry logic\n\n## File Locations\n\n### Frontend\n\n**Models:**\n- `projects/ngx-ramblers/src/app/models/content-text.model.ts` - Template types, mapping interfaces\n\n**Components:**\n- `projects/ngx-ramblers/src/app/modules/common/dynamic-content/dynamic-content-site-edit.ts` - Template UI, mapping mode\n- `projects/ngx-ramblers/src/app/modules/common/dynamic-content/dynamic-content-site-edit-text-row.ts` - Column mapping UI\n- `projects/ngx-ramblers/src/app/pages/admin/system-settings/migration/migration-settings.ts` - Parent page configuration\n\n**Services:**\n- `projects/ngx-ramblers/src/app/pages/admin/data-population.service.ts` - Template descriptions\n\n### Backend\n\n**Migration Engine:**\n- `server/lib/migration/page-transformation-engine.ts` - Template transformation engine\n  - `transformWithTemplate()` method (line ~1600)\n  - Column mapping processing (line ~1750)\n  - Content extraction and nested row generation\n\n**Migrations:**\n- `server/lib/mongo/migrations/database/20251121090000-rename-fragment-index-to-content-templates.ts` - Template library setup\n\n**Migration Service:**\n- `server/lib/migration/migrate-static-site-engine.ts` - Site migration orchestration\n  - Template loading and caching\n  - Parent page template override logic\n\n## Routes Template Example\n\nThe repository includes `docs/routes-template-page-content.json` as a reference implementation containing:\n\n- Full `fragments/templates/routes-template` structure\n- Populated rows based on example walk\n- Mapping definitions for title, location, map, narrative, and sidebar\n- Clean markdown with helper text removed\n\n**To use:**\n1. Open `/admin/page-content/fragments/templates/routes-template`\n2. Click **Paste page content**\n3. Paste JSON from `docs/routes-template-page-content.json`\n4. Click **Apply pasted content**\n5. Save template\n\nThis gives you a working baseline to experiment with mappings and configurations.\n\n## Related Documentation\n\n- **Database Migrations**: See `docs/admin/MIGRATIONS.md` for MongoDB schema migrations (different topic)\n- **Phase 7 Overview**: Historical context in `docs/MIGRATION_PHASE_7.md` (superseded by this guide)\n","contentHtml":"<h1>Content Migration Guide</h1>\n<h2>Overview</h2>\n<p>The Content Migration system provides a template-based workflow for migrating pages from external websites into your NGX-Ramblers site structure. Instead of hard-coded parsing logic or abstract transformation steps, you:</p>\n<ol>\n<li><strong>Build a visual template</strong> using the page editor</li>\n<li><strong>Configure data mappings</strong> for each element (where content comes from)</li>\n<li><strong>Apply the template</strong> to migrate multiple pages automatically</li>\n</ol>\n<h3>Key Features</h3>\n<ul>\n<li><strong>Visual template creation</strong>: What you build is what migrated pages will look like</li>\n<li><strong>Declarative mappings</strong>: Configure extraction rules without writing code</li>\n<li><strong>Dynamic content generation</strong>: Automatically create nested rows from scraped content</li>\n<li><strong>Reusable templates</strong>: One template can migrate hundreds of pages</li>\n<li><strong>Parent page support</strong>: Create index pages with action buttons</li>\n<li><strong>Pattern matching</strong>: Extract specific content using text/image patterns</li>\n</ul>\n<h3>Template Types</h3>\n<ul>\n<li><strong>Shared Fragment</strong>: Reusable components (headers, footers, sidebars) inserted into multiple pages</li>\n<li><strong>User Template</strong>: Custom page layouts for quickly building new pages with consistent structure</li>\n<li><strong>Migration Template</strong>: Templates with data mappings for migrating content from external sources</li>\n</ul>\n<h2>Quick Start</h2>\n<h3>1. Create a Migration Template</h3>\n<ol>\n<li>Navigate to a page in site edit mode (e.g., <code>/routes/example-route</code>)</li>\n<li>Build your ideal structure using the page editor:<ul>\n<li>Add rows (Text, Location, Map, Index)</li>\n<li>Arrange them exactly as you want migrated pages to look</li>\n<li>Configure row settings (height, columns, visibility)</li>\n</ul>\n</li>\n<li>Enable <strong>Template Mode</strong>:<ul>\n<li>Toggle &quot;Template options&quot; switch (top right)</li>\n<li>Click the <strong>&quot;Migration template&quot;</strong> badge button</li>\n</ul>\n</li>\n<li>Enable <strong>Mapping Mode</strong>:<ul>\n<li>Toggle <strong>&quot;Mapping mode&quot;</strong> switch</li>\n<li>This reveals mapping configuration UI for each row</li>\n</ul>\n</li>\n<li>Configure mappings (see sections below)</li>\n<li>Click <strong>&quot;Create template&quot;</strong> to publish</li>\n</ol>\n<h3>2. Configure Parent Pages</h3>\n<p>Navigate to <strong>Admin → System Settings → Migration → Settings tab</strong></p>\n<ol>\n<li>Add a parent page for your migration</li>\n<li>Configure the settings (see Parent Page Configuration section)</li>\n<li>Select your migration template from the &quot;Override Template&quot; dropdown</li>\n<li>Save configuration</li>\n</ol>\n<h3>3. Run Migration</h3>\n<ol>\n<li>Stay in Migration Settings</li>\n<li>Configure which pages to migrate</li>\n<li>Click <strong>Run migration</strong></li>\n<li>Monitor progress in the Activity tab</li>\n</ol>\n<h2>Parent Page Configuration</h2>\n<p>Each migration site can have multiple <strong>Parent Pages</strong> - these are index/category pages that contain lists of child pages to migrate.</p>\n<h3>Parent Page Settings</h3>\n<p>For each parent page, configure:</p>\n<p><strong>URL Pattern</strong>: The parent page URL</p>\n<ul>\n<li>Example: <code>/walks/routes</code></li>\n<li>The source page containing links to child pages</li>\n</ul>\n<p><strong>Path Prefix</strong>: Prefix for child page paths</p>\n<ul>\n<li>Example: <code>walks/routes/</code></li>\n<li>Used to construct destination paths for migrated pages</li>\n</ul>\n<p><strong>Link Selector</strong>: CSS selector for child page links (optional)</p>\n<ul>\n<li>Defaults to content area if empty</li>\n<li>Example: <code>.route-list a</code> to target specific links</li>\n</ul>\n<p><strong>Max Child Pages</strong>: Limit number of children to migrate (optional)</p>\n<ul>\n<li>Example: <code>50</code> to migrate only first 50 pages</li>\n<li>Leave empty to migrate all found pages</li>\n</ul>\n<h3>Migrate Parent Page Options</h3>\n<p>Controls what happens to the parent page itself during migration:</p>\n<h4>Not migrated (default)</h4>\n<ul>\n<li>The parent page <strong>itself is skipped</strong></li>\n<li>Child pages <strong>are still migrated</strong></li>\n<li>Use when: Parent page doesn&#39;t exist or you&#39;ll create it manually</li>\n<li>Example: Skip <code>/walks/routes</code> but migrate <code>/walks/routes/wealdway</code></li>\n</ul>\n<h4>As-is</h4>\n<ul>\n<li>Parent page <strong>is migrated</strong> with its original content</li>\n<li>Content extracted and migrated like any other page</li>\n<li>Use when: Parent page has intro text or useful content to preserve</li>\n<li>Example: <code>/walks/routes</code> has introductory text about routes</li>\n</ul>\n<h4>With Links as Action Buttons Row</h4>\n<ul>\n<li>Parent page <strong>is migrated</strong> with child page links converted to action buttons</li>\n<li>Creates an <strong>auto-generated index page</strong></li>\n<li>Use when: You want a navigation/directory page</li>\n<li>Example: <code>/walks/routes</code> becomes a page with [Weald Way] [North Downs Way] [Thames Path] buttons</li>\n</ul>\n<h3>Override Template (optional)</h3>\n<p>By default, child pages use the site&#39;s default migration template. Use this field to specify a <strong>different template</strong> for children under this parent page.</p>\n<p><strong>Use cases:</strong></p>\n<ul>\n<li>Different page structures for different sections<ul>\n<li>Routes use &quot;routes-template&quot;</li>\n<li>Regional routes use &quot;regional-routes-template&quot;</li>\n</ul>\n</li>\n<li>Special layouts for specific categories<ul>\n<li>Index pages with summaries</li>\n<li>Detail pages with maps</li>\n</ul>\n</li>\n</ul>\n<p><strong>How it works:</strong></p>\n<ul>\n<li>Select a template from the dropdown</li>\n<li>All child pages under this parent use the selected template</li>\n<li>If empty, child pages use the site&#39;s default template</li>\n<li>Template selection shows all available migration templates from your <code>fragments/templates/</code> folder</li>\n</ul>\n<p><strong>Example configuration:</strong></p>\n<pre><code>Parent Page: /walks/routes\n├─ Migrate parent page: With Links as Action Buttons Row\n├─ Override Template: routes-template\n└─ Max child pages: 50\n\nResult:\n- /walks/routes → Index page with action buttons\n- /walks/routes/wealdway → Uses routes-template\n- /walks/routes/north-downs-way → Uses routes-template\n</code></pre>\n<h2>Template Creation Workflow</h2>\n<h3>UI Components</h3>\n<h4>Template Options Panel</h4>\n<p>Located at top of page editor when &quot;Template options&quot; toggle is ON:</p>\n<pre><code>[Shared fragment][User template][Migration template] [Remove] [Create template] [Mapping mode ☑]\n</code></pre>\n<ul>\n<li><strong>Type badges</strong>: Select template type</li>\n<li><strong>Remove</strong>: Clear template configuration</li>\n<li><strong>Create template</strong>: Publish to fragment library</li>\n<li><strong>Mapping mode</strong>: Toggle migration mapping UI (migration templates only)</li>\n</ul>\n<h4>Template Library Panel</h4>\n<p>Shows published templates for reuse:</p>\n<pre><code>Template library\n├─ Dropdown (shows fragment paths)\n└─ [Replace] [Append]\n</code></pre>\n<ul>\n<li><strong>Replace</strong>: Replace current page content with template</li>\n<li><strong>Append</strong>: Append template to current page</li>\n</ul>\n<h4>Paste Page Content</h4>\n<p>The page editor includes a <strong>Paste page content</strong> badge next to save/revert buttons. Use this to:</p>\n<ul>\n<li>Clone complex templates between environments</li>\n<li>Try out example content (like <code>docs/routes-template-page-content.json</code>)</li>\n<li>Restore earlier snapshots from API explorer</li>\n</ul>\n<p>The parser automatically unwraps API response shapes, so you can paste output from <code>/api/page-content?path=...</code> directly.</p>\n<h2>Data Mapping Configuration</h2>\n<h3>Row-Level Mappings</h3>\n<p>When Mapping Mode is enabled, each row shows configuration based on its type.</p>\n<h4>Location Row Mapping</h4>\n<pre><code>Source type: [Extract] [Static] [Metadata]\n☑ Extract from content\n☑ Hide location row\nDefault location: [____________]\n</code></pre>\n<p><strong>Extract from content</strong>: Automatically extract location from narrative text</p>\n<ul>\n<li>Looks for postcodes, grid references, place names</li>\n<li>Geocodes to latitude/longitude</li>\n</ul>\n<p><strong>Hide location row</strong>: Make the row invisible</p>\n<ul>\n<li>Row data still available for Map rows via <code>useLocationFromRow</code></li>\n<li>Use when location is only for map positioning, not display</li>\n</ul>\n<p><strong>Default location</strong>: Fallback location if extraction fails</p>\n<ul>\n<li>Example: &quot;Tonbridge, Kent&quot; for Kent-based routes</li>\n<li>Ensures map always has a center point</li>\n</ul>\n<h4>Map Row Mapping</h4>\n<pre><code>☑ Extract GPX from content\nGPX file path: [____________]\nUse location from row: [0]\n</code></pre>\n<p><strong>Extract GPX from content</strong>: Pull GPX file from source page</p>\n<ul>\n<li>Searches for .gpx file links in source HTML</li>\n<li>Downloads and processes route data</li>\n</ul>\n<p><strong>GPX file path</strong>: Static GPX file path (alternative to extraction)</p>\n<ul>\n<li>Example: <code>/assets/routes/wealdway.gpx</code></li>\n</ul>\n<p><strong>Use location from row</strong>: Row index to get location coordinates from</p>\n<ul>\n<li>Example: <code>0</code> references first row (Location row)</li>\n<li>Map centers on this location</li>\n<li>Supports nested row references (e.g., <code>0.1</code> = row 0, nested row 1)</li>\n</ul>\n<h4>Metadata Row Mapping</h4>\n<pre><code>Source type: [Metadata ▼]\nField: [Title | Path | Migration note]\nPrefix: [Automatically migrated from]\nDate format: [yyyy-LL-dd HH:mm]\n</code></pre>\n<p><strong>Field options:</strong></p>\n<ul>\n<li><code>Title</code>: Page title from scraped metadata</li>\n<li><code>Path</code>: Source page URL/path</li>\n<li><code>Migration note</code>: &quot;Migrated from [URL] on [date]&quot; footer</li>\n</ul>\n<p><strong>Customization:</strong></p>\n<ul>\n<li><code>metadataPrefix</code>: Custom prefix text (default: &quot;Migrated from&quot;)</li>\n<li><code>metadataDateFormat</code>: Luxon format string (default: &quot;yyyy-LL-dd HH:mm&quot;)</li>\n</ul>\n<h4>Index Row Mapping</h4>\n<p>Index rows use existing Index row settings:</p>\n<ul>\n<li>Content types (walks, routes, social events)</li>\n<li>Render modes (carousel, grid, list)</li>\n<li>Map settings (clustering, markers)</li>\n</ul>\n<p>No additional mapping configuration required.</p>\n<h3>Column-Level Mappings</h3>\n<p>For rows with columns containing nested rows, column-level mapping configuration appears.</p>\n<h4>Column Mapping UI</h4>\n<pre><code>Column 2 Mapping\n─────────────────────────────────────\nSource Type: [Extract | Static | Not mapped]\nContent Type: [Text | Image | Mixed]\n\nNested Rows Configuration\n─────────────────────────────────────\nContent source: [Extract from content ▼]\nPacking behavior: [One row per item ▼]\n☑ Group text with images\n\nDocumentation notes\n[Notes about what this mapping does...]\n</code></pre>\n<h4>Content Source Options</h4>\n<p><strong>Not mapped</strong></p>\n<ul>\n<li>Column stays exactly as designed in template</li>\n<li>Use for static content (headers, footers, shared fragments)</li>\n</ul>\n<p><strong>Extract from content</strong></p>\n<ul>\n<li>Populate from scraped page data</li>\n<li>Requires Content Type and packing configuration</li>\n</ul>\n<p><strong>Static content</strong></p>\n<ul>\n<li>Use template content as-is</li>\n<li>Allows adding documentation notes</li>\n</ul>\n<h4>Content Type (what to extract)</h4>\n<p><strong>Remaining images</strong></p>\n<ul>\n<li>Images not yet consumed by other mappings</li>\n<li>Recommended for galleries to avoid duplication</li>\n<li>Each column gets unique images</li>\n</ul>\n<p><strong>Remaining text</strong></p>\n<ul>\n<li>Text segments (paragraphs, headings) still unused</li>\n<li>Recommended catch-all for narrative text</li>\n<li>Prevents double-rendering content</li>\n</ul>\n<p><strong>All content</strong></p>\n<ul>\n<li>Every text and image segment in source order</li>\n<li>Use when column should mirror original story exactly</li>\n</ul>\n<p><strong>All images</strong></p>\n<ul>\n<li>Every image, even if already used elsewhere</li>\n<li>Use when multiple galleries need same images</li>\n</ul>\n<p><strong>Pattern match</strong></p>\n<ul>\n<li>Only segments matching custom pattern</li>\n<li>Supports regex, filename glob, CSS selector</li>\n<li>Pattern input appears when selected</li>\n</ul>\n<h4>Packing Behavior (when extracting)</h4>\n<p><strong>One row per item</strong></p>\n<ul>\n<li>Create separate nested row for each extracted item</li>\n<li>Example: One nested row per image</li>\n<li>Repeats until no matches remain</li>\n</ul>\n<p><strong>All in one row</strong></p>\n<ul>\n<li>Put all extracted content in single nested row</li>\n<li>Example: All narrative text in one row</li>\n<li>Produces single combined row</li>\n</ul>\n<p><strong>Collect with breaks</strong></p>\n<ul>\n<li>Group content based on document structure</li>\n<li>Smart breaking on headings, images, paragraphs</li>\n<li>Configurable break points</li>\n</ul>\n<h4>Additional Options</h4>\n<p><strong>Group text with images</strong> (<code>groupTextWithImage</code>)</p>\n<ul>\n<li>Associates caption text with each image</li>\n<li>Looks for text immediately following image</li>\n<li>Critical for preserving image captions</li>\n</ul>\n<p><strong>Hide row if empty</strong> (<code>hideIfEmpty</code>)</p>\n<ul>\n<li>Removes entire row when no content extracted</li>\n<li>Prevents blank placeholders</li>\n<li>Useful for optional sections</li>\n</ul>\n<p><strong>Documentation notes</strong></p>\n<ul>\n<li>Explain what this mapping does</li>\n<li>Visible in mapping mode</li>\n<li>Example: &quot;Points-of-interest sidebar with remaining images&quot;</li>\n</ul>\n<h3>Dynamic Target Rows</h3>\n<p>When Mapping mode is enabled, you can mark any nested row as a <strong>Dynamic target</strong> using the badge button in the nested row header (stacked-layers icon).</p>\n<p><strong>What it does:</strong></p>\n<ul>\n<li>Marks which nested row will be cloned and populated with extracted content</li>\n<li>The selected row becomes the template for dynamically generated rows</li>\n<li>Only one nested row can be the dynamic target per column</li>\n</ul>\n<p><strong>How it works:</strong></p>\n<ol>\n<li>Content Source specifies <strong>what</strong> to extract and <strong>how</strong> to pack it</li>\n<li>Dynamic target button specifies <strong>which row structure</strong> to use</li>\n<li>When migration runs:<ul>\n<li>Extracts content based on Content Type setting</li>\n<li>Clones the target nested row</li>\n<li>Fills clone with extracted content</li>\n<li>Repeats if packing behavior is &quot;one-per-item&quot;</li>\n</ul>\n</li>\n<li>Other nested rows (without button) are static and added after dynamic rows</li>\n</ol>\n<p><strong>Example:</strong></p>\n<pre><code>Nested Rows Configuration:\n├─ Content source: remaining-images\n├─ Packing: one-per-item\n│\n├─ Nested Row 1 (Text) ← &quot;Use as dynamic target&quot; (active)\n│  └─ Column: image + caption placeholder\n│\n└─ Nested Row 2 (Shared Fragment) ← Static row\n    └─ Shared fragment reference\n</code></pre>\n<p><strong>Result:</strong></p>\n<ul>\n<li>Nested row 1 cloned for each remaining image</li>\n<li>Each clone filled with image + caption</li>\n<li>Nested row 2 (shared fragment) added once at end</li>\n</ul>\n<h3>Column Processing Order</h3>\n<p><strong>Important:</strong> When a row has multiple columns with dynamic collection, order matters:</p>\n<ol>\n<li><strong>Columns with <code>remaining-images</code></strong> processed <strong>first</strong><ul>\n<li>Allows images to capture their caption text</li>\n</ul>\n</li>\n<li><strong>Columns with <code>remaining-text</code></strong> processed <strong>second</strong><ul>\n<li>Gets narrative text after images claimed captions</li>\n</ul>\n</li>\n</ol>\n<p>This ensures images grab caption text before remaining-text column consumes all text.</p>\n<p><strong>Implementation:</strong> The system automatically sorts column mappings by priority:</p>\n<ol>\n<li>Pattern-match images (specific content)</li>\n<li>Remaining-images with <code>groupTextWithImage: true</code></li>\n<li>Other content types</li>\n<li>Remaining-images without text grouping</li>\n</ol>\n<h2>Content Matchers &amp; Patterns</h2>\n<h3>Text Patterns</h3>\n<p>Used when Content Type is <code>TEXT</code>:</p>\n<p><strong>ALL_TEXT_UNTIL_IMAGE</strong></p>\n<ul>\n<li>Extract all text from current position until first image</li>\n<li>Commonly used for intro paragraphs</li>\n<li>Stops at first image boundary</li>\n</ul>\n<p><strong>ALL_TEXT_AFTER_HEADING</strong></p>\n<ul>\n<li>Extract heading and following text until next heading or image</li>\n<li>Good for section extraction</li>\n<li>Captures semantic blocks</li>\n</ul>\n<p><strong>REMAINING_TEXT</strong></p>\n<ul>\n<li>Extract all remaining unused text content</li>\n<li>Use at end of transformation to capture everything left</li>\n<li>Ensures no content is lost</li>\n</ul>\n<p><strong>PARAGRAPH</strong></p>\n<ul>\n<li>Extract single paragraph or text segment</li>\n<li>Advances to next segment on each use</li>\n<li>For granular text extraction</li>\n</ul>\n<p><strong>STARTS_WITH_HEADING</strong></p>\n<ul>\n<li>Extract text beginning with markdown heading (# through ######)</li>\n<li>Filters to heading-led sections</li>\n<li>Useful for structured content</li>\n</ul>\n<p><strong>CUSTOM_REGEX</strong></p>\n<ul>\n<li>Match text using custom regular expression</li>\n<li>Requires <code>customRegex</code> field</li>\n<li>Powerful for complex extraction patterns</li>\n<li>Test patterns using regex101.com</li>\n</ul>\n<h3>Image Patterns</h3>\n<p>Used when Content Type is <code>IMAGE</code>:</p>\n<p><strong>FIRST_IMAGE</strong></p>\n<ul>\n<li>Extract first unused image</li>\n<li>Most commonly used pattern</li>\n<li>Marks image as used</li>\n</ul>\n<p><strong>REMAINING_IMAGES</strong></p>\n<ul>\n<li>Extract ALL remaining unused images</li>\n<li>Marks all as used</li>\n<li>Use when consuming all remaining images</li>\n</ul>\n<p><strong>ALL_IMAGES</strong></p>\n<ul>\n<li>Extract ALL images regardless of used status</li>\n<li>Does NOT mark as used</li>\n<li>Useful for showing images in multiple places (carousel + gallery)</li>\n</ul>\n<p><strong>FILENAME_PATTERN</strong></p>\n<ul>\n<li>Match images by filename using wildcards</li>\n<li>Requires <code>filenamePattern</code> field</li>\n<li>Example: <code>*route-map*.jpg</code></li>\n<li>Case-sensitive matching</li>\n</ul>\n<p><strong>ALT_TEXT_PATTERN</strong></p>\n<ul>\n<li>Match images by alt text using regex</li>\n<li>Requires <code>altTextPattern</code> field</li>\n<li>Example: <code>^Map of.*</code></li>\n<li>Useful for semantic image selection</li>\n</ul>\n<h3>Pattern Usage Table</h3>\n<table>\n<thead>\n<tr>\n<th>Pattern</th>\n<th>Use Case</th>\n<th>Marks as Used?</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>FIRST_IMAGE</code></td>\n<td>Single hero image, featured image</td>\n<td>Yes</td>\n</tr>\n<tr>\n<td><code>REMAINING_IMAGES</code></td>\n<td>Gallery consuming all remaining</td>\n<td>Yes</td>\n</tr>\n<tr>\n<td><code>ALL_IMAGES</code></td>\n<td>Show all images in carousel, then detail</td>\n<td>No</td>\n</tr>\n<tr>\n<td><code>FILENAME_PATTERN</code></td>\n<td>Specific image like route map</td>\n<td>Yes</td>\n</tr>\n<tr>\n<td><code>ALT_TEXT_PATTERN</code></td>\n<td>Images with specific alt text</td>\n<td>Yes</td>\n</tr>\n</tbody></table>\n<h3>Understanding &quot;Used&quot; vs &quot;Unused&quot; Content</h3>\n<p>The transformation engine tracks which content has been consumed:</p>\n<p><strong>Text Content:</strong></p>\n<ul>\n<li>Starts with all markdown text segments marked as &quot;unused&quot;</li>\n<li>When text pattern matches and extracts content, segments marked as &quot;used&quot;</li>\n<li><code>REMAINING_TEXT</code> extracts all segments still marked &quot;unused&quot;</li>\n</ul>\n<p><strong>Image Content:</strong></p>\n<ul>\n<li>All scraped images start as &quot;unused&quot;</li>\n<li><code>FIRST_IMAGE</code> extracts first unused image and marks it used</li>\n<li><code>REMAINING_IMAGES</code> extracts ALL unused images and marks all used</li>\n<li><code>ALL_IMAGES</code> extracts ALL images but does NOT mark them used (allows reuse)</li>\n</ul>\n<h3>Best Practices for Content Extraction</h3>\n<p><strong>1. Extract in Order of Specificity</strong></p>\n<pre><code>Step 1: Extract specific content (e.g., route map by filename)\nStep 2: Extract structured content (e.g., intro text until image)\nStep 3: Extract remaining content (e.g., remaining-text, remaining-images)\n</code></pre>\n<p><strong>2. Use REMAINING Patterns Last</strong>\nAlways use <code>REMAINING_TEXT</code> or <code>REMAINING_IMAGES</code> in final rows to ensure no content is lost.</p>\n<p><strong>3. Test Custom Regex Incrementally</strong>\nStart simple and add complexity:</p>\n<ul>\n<li>Test: <code>^###</code> (just headings)</li>\n<li>Then: <code>^###\\\\s+.+</code> (heading with text)</li>\n<li>Finally: <code>^###\\\\s+.+[\\\\s\\\\S]*?(?=###|$)</code> (heading with all text until next heading)</li>\n</ul>\n<p><strong>4. Filename Patterns Are Case-Sensitive</strong></p>\n<ul>\n<li>Use lowercase patterns: <code>*route-map*</code> not <code>*Route-Map*</code></li>\n<li>Check actual filenames in Activity log output</li>\n<li>Wildcards: <code>*</code> matches any characters</li>\n</ul>\n<p><strong>5. Extract Images Before Text</strong>\nWhen both use <code>remaining-*</code> patterns, ensure images process first to capture captions before text consumes them.</p>\n<h2>Common Template Patterns</h2>\n<h3>Pattern: Two-Column Layout with Featured Image</h3>\n<pre><code>Row 1: Full-width intro text\nRow 2: Two columns\n  ├─ Column 1 (8 cols): Featured image\n  └─ Column 2 (4 cols): Remaining content in nested rows\n</code></pre>\n<p><strong>Mapping:</strong></p>\n<ul>\n<li>Row 1, Column 1: Text pattern = <code>ALL_TEXT_UNTIL_IMAGE</code></li>\n<li>Row 2, Column 1: Image pattern = <code>FIRST_IMAGE</code></li>\n<li>Row 2, Column 2: Content source = <code>remaining-text</code>, Packing = <code>all-in-one</code></li>\n</ul>\n<h3>Pattern: Route Page with Hidden Location</h3>\n<pre><code>Row 1: Location (hidden)\nRow 2: Map (references Row 1)\nRow 3: Two columns (narrative + sidebar)\n  ├─ Column 1 (8 cols): Main narrative text\n  └─ Column 2 (4 cols): Points of interest images\n</code></pre>\n<p><strong>Mapping:</strong></p>\n<ul>\n<li>Row 1: Extract from content ☑, Hide row ☑</li>\n<li>Row 2: Use location from row = <code>0</code></li>\n<li>Row 3, Column 1: Content source = <code>remaining-text</code>, Packing = <code>all-in-one</code></li>\n<li>Row 3, Column 2: Content source = <code>remaining-images</code>, Packing = <code>one-per-item</code>, Group text with images ☑</li>\n</ul>\n<h3>Pattern: Photo Gallery with Map</h3>\n<pre><code>Row 1: Two columns\n  ├─ Column 1 (6 cols): Map showing photo locations\n  └─ Column 2 (6 cols): Dynamic gallery (one row per photo)\n</code></pre>\n<p><strong>Mapping:</strong></p>\n<ul>\n<li>Row 1, Column 1: Static (map configured in template)</li>\n<li>Row 1, Column 2: Content source = <code>remaining-images</code>, Packing = <code>one-per-item</code></li>\n</ul>\n<h3>Pattern: Index Page with Action Buttons</h3>\n<pre><code>Row 1: Intro text\nRow 2: Index row (maps + clustering)\nRow 3: Action buttons for child pages\n</code></pre>\n<p><strong>Parent Page:</strong></p>\n<ul>\n<li>Migrate parent page: <code>With Links as Action Buttons Row</code></li>\n<li>This automatically creates Row 3 with buttons</li>\n</ul>\n<h2>Advanced Topics</h2>\n<h3>Nested Row Mappings Data Model</h3>\n<pre><code class=\"language-typescript\">interface NestedRowMappingConfig {\n  contentSource: &#39;remaining-images&#39; | &#39;remaining-text&#39; | &#39;all-content&#39; | &#39;all-images&#39; | &#39;pattern-match&#39;;\n  packingBehavior: &#39;one-per-item&#39; | &#39;all-in-one&#39; | &#39;collect-with-breaks&#39;;\n  breakOn?: &#39;image&#39; | &#39;heading&#39; | &#39;paragraph&#39;;\n  stopOn?: &#39;image&#39; | &#39;heading&#39;;\n  groupTextWithImage?: boolean;\n  filenamePattern?: string;\n  textPattern?: string;\n  imagePattern?: string;\n}\n</code></pre>\n<h3>Column-Level Mappings Data Model</h3>\n<pre><code class=\"language-typescript\">interface ColumnMappingConfig {\n  columnIndex: number;\n  sourceType?: &#39;extract&#39; | &#39;static&#39; | &#39;metadata&#39;;\n  contentType?: &#39;text&#39; | &#39;image&#39; | &#39;mixed&#39;;\n  extractPreset?: string;\n  extractPattern?: string;\n  nestedRowMapping?: NestedRowMappingConfig;\n  notes?: string;\n}\n</code></pre>\n<h3>Row-Level Mappings Data Model</h3>\n<pre><code class=\"language-typescript\">interface MigrationTemplateMapping {\n  targetRowIndex: number;\n  sourceType?: &#39;extract&#39; | &#39;static&#39; | &#39;metadata&#39;;\n\n  // Type-specific mappings\n  location?: MigrationTemplateLocationMapping;\n  map?: MigrationTemplateMapMapping;\n  index?: MigrationTemplateIndexMapping;\n\n  columnMappings?: ColumnMappingConfig[];\n  hideIfEmpty?: boolean;\n  notes?: string;\n}\n</code></pre>\n<h3>Bootstrap Grid System</h3>\n<p>Remember the 12-column grid:</p>\n<ul>\n<li>Full width: 12</li>\n<li>Half width: 6</li>\n<li>Two-thirds: 8, one-third: 4</li>\n<li>Three columns: 4, 4, 4</li>\n</ul>\n<p>Columns in a row should sum to 12 for best results.</p>\n<h3>Image Styling</h3>\n<p>Images support:</p>\n<ul>\n<li><code>imageBorderRadius</code>: Border radius in pixels (default: 6)</li>\n<li><code>alt</code>: Alt text (auto-populated from scraped alt)</li>\n<li><code>imageSource</code>: URL to image file</li>\n</ul>\n<h3>Content Exclusions</h3>\n<p>Before transformation, filter content using site config:</p>\n<ul>\n<li><code>excludeSelectors</code>: Remove HTML elements before scraping</li>\n<li><code>excludeTextPatterns</code>: Remove text matching regex</li>\n<li><code>excludeMarkdownBlocks</code>: Remove exact markdown blocks</li>\n<li><code>excludeImageUrls</code>: Don&#39;t use specific images</li>\n</ul>\n<h2>Troubleshooting</h2>\n<h3>Template Issues</h3>\n<p><strong>Template not appearing in library</strong></p>\n<ul>\n<li>Check template was published (click &quot;Create template&quot;)</li>\n<li>Verify &quot;Template options&quot; toggle is ON</li>\n<li>Check fragment path doesn&#39;t conflict with existing content</li>\n</ul>\n<p><strong>Mapping mode not showing</strong></p>\n<ul>\n<li>Ensure &quot;Migration template&quot; type is selected</li>\n<li>Toggle &quot;Mapping mode&quot; switch</li>\n</ul>\n<h3>Content Extraction Issues</h3>\n<p><strong>Content not appearing</strong></p>\n<ul>\n<li>Check Activity log for matcher debug output</li>\n<li>Adjust pattern or use &quot;all&quot; to see all content first</li>\n<li>Verify content source (remaining vs all)</li>\n</ul>\n<p><strong>Wrong image selected</strong></p>\n<ul>\n<li>Filename pattern too broad or too narrow</li>\n<li>Check image filenames in scraped content</li>\n<li>Use more specific patterns</li>\n</ul>\n<p><strong>Images show placeholder text instead of captions</strong></p>\n<ul>\n<li>Set <code>groupTextWithImage: true</code> in nested row mapping</li>\n<li>Ensure images processed before remaining-text</li>\n</ul>\n<p><strong>Narrative column includes image caption headings</strong></p>\n<ul>\n<li>System auto-sorts to process images first</li>\n<li>Verify column processing order in mappings</li>\n</ul>\n<p><strong>Nested rows not populating</strong></p>\n<ul>\n<li>Check targetRow and targetColumn index (zero-based)</li>\n<li>Verify dynamic target row is marked</li>\n<li>Check content source pattern matches content</li>\n</ul>\n<p><strong>Text split incorrectly</strong></p>\n<ul>\n<li>Try different text patterns</li>\n<li><code>ALL_TEXT_UNTIL_IMAGE</code> stops at first image</li>\n<li><code>REMAINING_TEXT</code> takes everything unused</li>\n</ul>\n<p><strong>Custom regex not matching</strong></p>\n<ul>\n<li>Test regex using regex101.com</li>\n<li>Escape special characters</li>\n<li>Use <code>[\\\\s\\\\S]*?</code> for multiline matching</li>\n<li>Common patterns:<ul>\n<li>Match heading + text: <code>^###\\\\s+.+[\\\\s\\\\S]*?(?=###|$)</code></li>\n<li>Match paragraph: <code>^[^#\\\\n].+(?:\\\\n[^#\\\\n].+)*</code></li>\n</ul>\n</li>\n</ul>\n<p><strong>Filename pattern not matching images</strong></p>\n<ul>\n<li>Check actual filenames in scraped content</li>\n<li>Use wildcards correctly: <code>*route*</code> matches any file containing &quot;route&quot;</li>\n<li>Case-sensitive by default</li>\n<li>Patterns match against full URL path, not just filename</li>\n</ul>\n<p><strong>No nested rows generated</strong></p>\n<ul>\n<li>Content type pattern didn&#39;t match anything</li>\n<li>Check debug logs in Activity tab</li>\n<li>Verify content source (e.g., &quot;remaining-images&quot; vs &quot;all-images&quot;)</li>\n</ul>\n<p><strong>All content in one nested row when expecting multiple</strong></p>\n<ul>\n<li>Packing behavior set to &quot;all-in-one&quot;</li>\n<li>Change to &quot;one-per-item&quot; for separate rows</li>\n</ul>\n<h3>Location &amp; Map Issues</h3>\n<p><strong>Location extraction not working</strong></p>\n<ul>\n<li>Verify narrative contains postcode or grid reference</li>\n<li>Check extraction logs in browser console</li>\n<li>Use default location as fallback</li>\n</ul>\n<p><strong>Map not centering on location</strong></p>\n<ul>\n<li>Check <code>useLocationFromRow</code> index is correct</li>\n<li>Verify location row has valid coordinates</li>\n<li>Review location extraction success in logs</li>\n</ul>\n<p><strong>Blank maps showing</strong></p>\n<ul>\n<li>No valid location data extracted</li>\n<li>Maps now automatically hidden when no coordinates</li>\n<li>Check location row configuration</li>\n</ul>\n<h2>Example Workflows</h2>\n<h3>Example 1: Migrating Route Pages</h3>\n<p><strong>Goal:</strong> Migrate old route pages with location extraction, map display, and narrative content.</p>\n<p><strong>Template Structure:</strong></p>\n<pre><code>Row 0: Location (hidden, extracted from content)\nRow 1: Map (500px height, uses location from Row 0)\nRow 2: Two columns\n  ├─ Column 1 (8 cols): Main narrative text\n  └─ Column 2 (4 cols): Points of interest images with captions\nRow 3: Migration note (metadata)\n</code></pre>\n<p><strong>Mappings:</strong></p>\n<ol>\n<li><p>Row 0 (Location):</p>\n<ul>\n<li>Extract from content: ☑</li>\n<li>Hide row: ☑</li>\n<li>Default location: &quot;Kent, England&quot;</li>\n</ul>\n</li>\n<li><p>Row 1 (Map):</p>\n<ul>\n<li>Use location from row: <code>0</code></li>\n<li>Auto-fit bounds: ☑</li>\n</ul>\n</li>\n<li><p>Row 2, Column 1:</p>\n<ul>\n<li>Source type: Extract</li>\n<li>Content type: Text</li>\n<li>Nested rows: Content source = <code>remaining-text</code>, Packing = <code>all-in-one</code></li>\n</ul>\n</li>\n<li><p>Row 2, Column 2:</p>\n<ul>\n<li>Source type: Extract</li>\n<li>Content type: Mixed</li>\n<li>Nested rows: Content source = <code>remaining-images</code>, Packing = <code>one-per-item</code>, Group text ☑</li>\n<li>Notes: &quot;Points-of-interest sidebar with image captions&quot;</li>\n</ul>\n</li>\n<li><p>Row 3 (Metadata):</p>\n<ul>\n<li>Source type: Metadata</li>\n<li>Field: Migration note</li>\n<li>Prefix: &quot;Migrated from&quot;</li>\n</ul>\n</li>\n</ol>\n<p><strong>Parent Page Config:</strong></p>\n<ul>\n<li>URL: <code>/walks/routes</code></li>\n<li>Migrate parent page: <code>With Links as Action Buttons Row</code></li>\n<li>Override template: <code>routes-template</code></li>\n<li>Max children: 100</li>\n</ul>\n<p><strong>Result:</strong> All route pages get consistent structure with extracted locations, centered maps, narrative text, and image galleries with preserved captions.</p>\n<h3>Example 2: Photo Gallery Migration</h3>\n<p><strong>Goal:</strong> Complex gallery with map showing photo locations and dynamic nested rows.</p>\n<p><strong>Template Structure:</strong></p>\n<pre><code>Row 0: Title and description\nRow 1: Two columns\n  ├─ Column 1 (6 cols):\n  │   ├─ Nested Row 0: Description text\n  │   ├─ Nested Row 1: Location (hidden)\n  │   └─ Nested Row 2: Map (references nested row 1)\n  └─ Column 2 (6 cols): Dynamic gallery rows (one per photo)\n</code></pre>\n<p><strong>Mappings:</strong></p>\n<ol>\n<li><p>Row 1, Column 1:</p>\n<ul>\n<li>Source type: Static</li>\n<li>Nested rows configured in template</li>\n</ul>\n</li>\n<li><p>Row 1, Column 2:</p>\n<ul>\n<li>Source type: Extract</li>\n<li>Content source: <code>remaining-images</code></li>\n<li>Packing: <code>one-per-item</code></li>\n<li>Mark Nested Row 0 as dynamic target</li>\n</ul>\n</li>\n</ol>\n<p><strong>Result:</strong> Gallery page with map on left, dynamically generated photo rows on right (one row per image).</p>\n<h2>Implementation Status</h2>\n<h3>✅ Completed</h3>\n<ul>\n<li>Template type system (Shared/User/Migration)</li>\n<li>Template UI with badge button groups</li>\n<li>Template publishing to fragment library</li>\n<li>Location row mapping UI</li>\n<li>Map row mapping UI</li>\n<li>Metadata row mapping UI</li>\n<li>Column-level mapping UI for nested rows<ul>\n<li>Content source selection</li>\n<li>Packing behavior configuration</li>\n<li>Content type selection</li>\n<li>Pattern matching support</li>\n</ul>\n</li>\n<li>Dynamic target row selection</li>\n<li>Template descriptions</li>\n<li>Paste page content functionality</li>\n<li>Basic template transformation engine (<code>transformWithTemplate</code>)<ul>\n<li>Static row cloning</li>\n<li>Metadata population</li>\n<li>Location/Map row handling</li>\n</ul>\n</li>\n<li>Column mapping data model</li>\n<li>Migration runtime integration<ul>\n<li>Site-level template selection</li>\n<li>Parent page template override</li>\n<li>Template priority over legacy transformations</li>\n</ul>\n</li>\n<li>Dynamic nested row extraction<ul>\n<li>Column mappings drive row generation</li>\n<li>Shared fragments preserved</li>\n<li><code>hideIfEmpty</code> support</li>\n</ul>\n</li>\n<li>Column processing order optimization<ul>\n<li>Images processed before text</li>\n<li>Caption preservation for image galleries</li>\n</ul>\n</li>\n</ul>\n<h3>🚧 In Progress</h3>\n<ul>\n<li>GPX file extraction from content</li>\n<li>Advanced pattern matching for text extraction</li>\n<li>Enhanced location narrative parsing</li>\n</ul>\n<h3>📋 Planned</h3>\n<ul>\n<li>Template preview in migration settings</li>\n<li>Batch migration with templates</li>\n<li>Template validation</li>\n<li>Visual mapping UI with drag-and-drop</li>\n<li>Template marketplace for sharing</li>\n<li>Migration preview/diff view</li>\n<li>Rollback capability</li>\n<li>Template versioning</li>\n<li>Real-time migration progress tracking</li>\n<li>Error recovery and retry logic</li>\n</ul>\n<h2>File Locations</h2>\n<h3>Frontend</h3>\n<p><strong>Models:</strong></p>\n<ul>\n<li><code>projects/ngx-ramblers/src/app/models/content-text.model.ts</code> - Template types, mapping interfaces</li>\n</ul>\n<p><strong>Components:</strong></p>\n<ul>\n<li><code>projects/ngx-ramblers/src/app/modules/common/dynamic-content/dynamic-content-site-edit.ts</code> - Template UI, mapping mode</li>\n<li><code>projects/ngx-ramblers/src/app/modules/common/dynamic-content/dynamic-content-site-edit-text-row.ts</code> - Column mapping UI</li>\n<li><code>projects/ngx-ramblers/src/app/pages/admin/system-settings/migration/migration-settings.ts</code> - Parent page configuration</li>\n</ul>\n<p><strong>Services:</strong></p>\n<ul>\n<li><code>projects/ngx-ramblers/src/app/pages/admin/data-population.service.ts</code> - Template descriptions</li>\n</ul>\n<h3>Backend</h3>\n<p><strong>Migration Engine:</strong></p>\n<ul>\n<li><code>server/lib/migration/page-transformation-engine.ts</code> - Template transformation engine<ul>\n<li><code>transformWithTemplate()</code> method (line ~1600)</li>\n<li>Column mapping processing (line ~1750)</li>\n<li>Content extraction and nested row generation</li>\n</ul>\n</li>\n</ul>\n<p><strong>Migrations:</strong></p>\n<ul>\n<li><code>server/lib/mongo/migrations/database/20251121090000-rename-fragment-index-to-content-templates.ts</code> - Template library setup</li>\n</ul>\n<p><strong>Migration Service:</strong></p>\n<ul>\n<li><code>server/lib/migration/migrate-static-site-engine.ts</code> - Site migration orchestration<ul>\n<li>Template loading and caching</li>\n<li>Parent page template override logic</li>\n</ul>\n</li>\n</ul>\n<h2>Routes Template Example</h2>\n<p>The repository includes <code>docs/routes-template-page-content.json</code> as a reference implementation containing:</p>\n<ul>\n<li>Full <code>fragments/templates/routes-template</code> structure</li>\n<li>Populated rows based on example walk</li>\n<li>Mapping definitions for title, location, map, narrative, and sidebar</li>\n<li>Clean markdown with helper text removed</li>\n</ul>\n<p><strong>To use:</strong></p>\n<ol>\n<li>Open <code>/admin/page-content/fragments/templates/routes-template</code></li>\n<li>Click <strong>Paste page content</strong></li>\n<li>Paste JSON from <code>docs/routes-template-page-content.json</code></li>\n<li>Click <strong>Apply pasted content</strong></li>\n<li>Save template</li>\n</ol>\n<p>This gives you a working baseline to experiment with mappings and configurations.</p>\n<h2>Related Documentation</h2>\n<ul>\n<li><strong>Database Migrations</strong>: See <code>docs/admin/MIGRATIONS.md</code> for MongoDB schema migrations (different topic)</li>\n<li><strong>Phase 7 Overview</strong>: Historical context in <code>docs/MIGRATION_PHASE_7.md</code> (superseded by this guide)</li>\n</ul>\n"}