Content Migration Guide

Overview

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:

  1. Build a visual template using the page editor
  2. Configure data mappings for each element (where content comes from)
  3. Apply the template to migrate multiple pages automatically

Key Features

Template Types

Quick Start

1. Create a Migration Template

  1. Navigate to a page in site edit mode (e.g., /routes/example-route)
  2. Build your ideal structure using the page editor:
    • Add rows (Text, Location, Map, Index)
    • Arrange them exactly as you want migrated pages to look
    • Configure row settings (height, columns, visibility)
  3. Enable Template Mode:
    • Toggle "Template options" switch (top right)
    • Click the "Migration template" badge button
  4. Enable Mapping Mode:
    • Toggle "Mapping mode" switch
    • This reveals mapping configuration UI for each row
  5. Configure mappings (see sections below)
  6. Click "Create template" to publish

2. Configure Parent Pages

Navigate to Admin → System Settings → Migration → Settings tab

  1. Add a parent page for your migration
  2. Configure the settings (see Parent Page Configuration section)
  3. Select your migration template from the "Override Template" dropdown
  4. Save configuration

3. Run Migration

  1. Stay in Migration Settings
  2. Configure which pages to migrate
  3. Click Run migration
  4. Monitor progress in the Activity tab

Parent Page Configuration

Each migration site can have multiple Parent Pages - these are index/category pages that contain lists of child pages to migrate.

Parent Page Settings

For each parent page, configure:

URL Pattern: The parent page URL

Path Prefix: Prefix for child page paths

Link Selector: CSS selector for child page links (optional)

Max Child Pages: Limit number of children to migrate (optional)

Migrate Parent Page Options

Controls what happens to the parent page itself during migration:

Not migrated (default)

As-is

With Links as Action Buttons Row

Override Template (optional)

By default, child pages use the site's default migration template. Use this field to specify a different template for children under this parent page.

Use cases:

How it works:

Example configuration:

Parent Page: /walks/routes
├─ Migrate parent page: With Links as Action Buttons Row
├─ Override Template: routes-template
└─ Max child pages: 50

Result:
- /walks/routes → Index page with action buttons
- /walks/routes/wealdway → Uses routes-template
- /walks/routes/north-downs-way → Uses routes-template

Template Creation Workflow

UI Components

Template Options Panel

Located at top of page editor when "Template options" toggle is ON:

[Shared fragment][User template][Migration template] [Remove] [Create template] [Mapping mode ☑]

Template Library Panel

Shows published templates for reuse:

Template library
├─ Dropdown (shows fragment paths)
└─ [Replace] [Append]

Paste Page Content

The page editor includes a Paste page content badge next to save/revert buttons. Use this to:

The parser automatically unwraps API response shapes, so you can paste output from /api/page-content?path=... directly.

Data Mapping Configuration

Row-Level Mappings

When Mapping Mode is enabled, each row shows configuration based on its type.

Location Row Mapping

Source type: [Extract] [Static] [Metadata]
☑ Extract from content
☑ Hide location row
Default location: [____________]

Extract from content: Automatically extract location from narrative text

Hide location row: Make the row invisible

Default location: Fallback location if extraction fails

Map Row Mapping

☑ Extract GPX from content
GPX file path: [____________]
Use location from row: [0]

Extract GPX from content: Pull GPX file from source page

GPX file path: Static GPX file path (alternative to extraction)

Use location from row: Row index to get location coordinates from

Metadata Row Mapping

Source type: [Metadata ▼]
Field: [Title | Path | Migration note]
Prefix: [Automatically migrated from]
Date format: [yyyy-LL-dd HH:mm]

Field options:

Customization:

Index Row Mapping

Index rows use existing Index row settings:

No additional mapping configuration required.

Column-Level Mappings

For rows with columns containing nested rows, column-level mapping configuration appears.

Column Mapping UI

Column 2 Mapping
─────────────────────────────────────
Source Type: [Extract | Static | Not mapped]
Content Type: [Text | Image | Mixed]

Nested Rows Configuration
─────────────────────────────────────
Content source: [Extract from content ▼]
Packing behavior: [One row per item ▼]
☑ Group text with images

Documentation notes
[Notes about what this mapping does...]

Content Source Options

Not mapped

Extract from content

Static content

Content Type (what to extract)

Remaining images

Remaining text

All content

All images

Pattern match

Packing Behavior (when extracting)

One row per item

All in one row

Collect with breaks

Additional Options

Group text with images (groupTextWithImage)

Hide row if empty (hideIfEmpty)

Documentation notes

Dynamic Target Rows

When 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).

What it does:

How it works:

  1. Content Source specifies what to extract and how to pack it
  2. Dynamic target button specifies which row structure to use
  3. When migration runs:
    • Extracts content based on Content Type setting
    • Clones the target nested row
    • Fills clone with extracted content
    • Repeats if packing behavior is "one-per-item"
  4. Other nested rows (without button) are static and added after dynamic rows

Example:

Nested Rows Configuration:
├─ Content source: remaining-images
├─ Packing: one-per-item
│
├─ Nested Row 1 (Text) ← "Use as dynamic target" (active)
│  └─ Column: image + caption placeholder
│
└─ Nested Row 2 (Shared Fragment) ← Static row
    └─ Shared fragment reference

Result:

Column Processing Order

Important: When a row has multiple columns with dynamic collection, order matters:

  1. Columns with remaining-images processed first
    • Allows images to capture their caption text
  2. Columns with remaining-text processed second
    • Gets narrative text after images claimed captions

This ensures images grab caption text before remaining-text column consumes all text.

Implementation: The system automatically sorts column mappings by priority:

  1. Pattern-match images (specific content)
  2. Remaining-images with groupTextWithImage: true
  3. Other content types
  4. Remaining-images without text grouping

Content Matchers & Patterns

Text Patterns

Used when Content Type is TEXT:

ALL_TEXT_UNTIL_IMAGE

ALL_TEXT_AFTER_HEADING

REMAINING_TEXT

PARAGRAPH

STARTS_WITH_HEADING

CUSTOM_REGEX

Image Patterns

Used when Content Type is IMAGE:

FIRST_IMAGE

REMAINING_IMAGES

ALL_IMAGES

FILENAME_PATTERN

ALT_TEXT_PATTERN

Pattern Usage Table

Pattern Use Case Marks as Used?
FIRST_IMAGE Single hero image, featured image Yes
REMAINING_IMAGES Gallery consuming all remaining Yes
ALL_IMAGES Show all images in carousel, then detail No
FILENAME_PATTERN Specific image like route map Yes
ALT_TEXT_PATTERN Images with specific alt text Yes

Understanding "Used" vs "Unused" Content

The transformation engine tracks which content has been consumed:

Text Content:

Image Content:

Best Practices for Content Extraction

1. Extract in Order of Specificity

Step 1: Extract specific content (e.g., route map by filename)
Step 2: Extract structured content (e.g., intro text until image)
Step 3: Extract remaining content (e.g., remaining-text, remaining-images)

2. Use REMAINING Patterns Last Always use REMAINING_TEXT or REMAINING_IMAGES in final rows to ensure no content is lost.

3. Test Custom Regex Incrementally Start simple and add complexity:

4. Filename Patterns Are Case-Sensitive

5. Extract Images Before Text When both use remaining-* patterns, ensure images process first to capture captions before text consumes them.

Common Template Patterns

Pattern: Two-Column Layout with Featured Image

Row 1: Full-width intro text
Row 2: Two columns
  ├─ Column 1 (8 cols): Featured image
  └─ Column 2 (4 cols): Remaining content in nested rows

Mapping:

Pattern: Route Page with Hidden Location

Row 1: Location (hidden)
Row 2: Map (references Row 1)
Row 3: Two columns (narrative + sidebar)
  ├─ Column 1 (8 cols): Main narrative text
  └─ Column 2 (4 cols): Points of interest images

Mapping:

Pattern: Photo Gallery with Map

Row 1: Two columns
  ├─ Column 1 (6 cols): Map showing photo locations
  └─ Column 2 (6 cols): Dynamic gallery (one row per photo)

Mapping:

Pattern: Index Page with Action Buttons

Row 1: Intro text
Row 2: Index row (maps + clustering)
Row 3: Action buttons for child pages

Parent Page:

Advanced Topics

Nested Row Mappings Data Model

interface NestedRowMappingConfig {
  contentSource: 'remaining-images' | 'remaining-text' | 'all-content' | 'all-images' | 'pattern-match';
  packingBehavior: 'one-per-item' | 'all-in-one' | 'collect-with-breaks';
  breakOn?: 'image' | 'heading' | 'paragraph';
  stopOn?: 'image' | 'heading';
  groupTextWithImage?: boolean;
  filenamePattern?: string;
  textPattern?: string;
  imagePattern?: string;
}

Column-Level Mappings Data Model

interface ColumnMappingConfig {
  columnIndex: number;
  sourceType?: 'extract' | 'static' | 'metadata';
  contentType?: 'text' | 'image' | 'mixed';
  extractPreset?: string;
  extractPattern?: string;
  nestedRowMapping?: NestedRowMappingConfig;
  notes?: string;
}

Row-Level Mappings Data Model

interface MigrationTemplateMapping {
  targetRowIndex: number;
  sourceType?: 'extract' | 'static' | 'metadata';

  // Type-specific mappings
  location?: MigrationTemplateLocationMapping;
  map?: MigrationTemplateMapMapping;
  index?: MigrationTemplateIndexMapping;

  columnMappings?: ColumnMappingConfig[];
  hideIfEmpty?: boolean;
  notes?: string;
}

Bootstrap Grid System

Remember the 12-column grid:

Columns in a row should sum to 12 for best results.

Image Styling

Images support:

Content Exclusions

Before transformation, filter content using site config:

Troubleshooting

Template Issues

Template not appearing in library

Mapping mode not showing

Content Extraction Issues

Content not appearing

Wrong image selected

Images show placeholder text instead of captions

Narrative column includes image caption headings

Nested rows not populating

Text split incorrectly

Custom regex not matching

Filename pattern not matching images

No nested rows generated

All content in one nested row when expecting multiple

Location & Map Issues

Location extraction not working

Map not centering on location

Blank maps showing

Example Workflows

Example 1: Migrating Route Pages

Goal: Migrate old route pages with location extraction, map display, and narrative content.

Template Structure:

Row 0: Location (hidden, extracted from content)
Row 1: Map (500px height, uses location from Row 0)
Row 2: Two columns
  ├─ Column 1 (8 cols): Main narrative text
  └─ Column 2 (4 cols): Points of interest images with captions
Row 3: Migration note (metadata)

Mappings:

  1. Row 0 (Location):

    • Extract from content: ☑
    • Hide row: ☑
    • Default location: "Kent, England"
  2. Row 1 (Map):

    • Use location from row: 0
    • Auto-fit bounds: ☑
  3. Row 2, Column 1:

    • Source type: Extract
    • Content type: Text
    • Nested rows: Content source = remaining-text, Packing = all-in-one
  4. Row 2, Column 2:

    • Source type: Extract
    • Content type: Mixed
    • Nested rows: Content source = remaining-images, Packing = one-per-item, Group text ☑
    • Notes: "Points-of-interest sidebar with image captions"
  5. Row 3 (Metadata):

    • Source type: Metadata
    • Field: Migration note
    • Prefix: "Migrated from"

Parent Page Config:

Result: All route pages get consistent structure with extracted locations, centered maps, narrative text, and image galleries with preserved captions.

Example 2: Photo Gallery Migration

Goal: Complex gallery with map showing photo locations and dynamic nested rows.

Template Structure:

Row 0: Title and description
Row 1: Two columns
  ├─ Column 1 (6 cols):
  │   ├─ Nested Row 0: Description text
  │   ├─ Nested Row 1: Location (hidden)
  │   └─ Nested Row 2: Map (references nested row 1)
  └─ Column 2 (6 cols): Dynamic gallery rows (one per photo)

Mappings:

  1. Row 1, Column 1:

    • Source type: Static
    • Nested rows configured in template
  2. Row 1, Column 2:

    • Source type: Extract
    • Content source: remaining-images
    • Packing: one-per-item
    • Mark Nested Row 0 as dynamic target

Result: Gallery page with map on left, dynamically generated photo rows on right (one row per image).

Implementation Status

✅ Completed

🚧 In Progress

📋 Planned

File Locations

Frontend

Models:

Components:

Services:

Backend

Migration Engine:

Migrations:

Migration Service:

Routes Template Example

The repository includes docs/routes-template-page-content.json as a reference implementation containing:

To use:

  1. Open /admin/page-content/fragments/templates/routes-template
  2. Click Paste page content
  3. Paste JSON from docs/routes-template-page-content.json
  4. Click Apply pasted content
  5. Save template

This gives you a working baseline to experiment with mappings and configurations.

Related Documentation