Skip to content

Bloomreach to Textpattern Migration: A Technical Guide

A technical guide to migrating from Bloomreach to Textpattern — covering API extraction, content flattening, MySQL loading, and media re-hosting.

Nachi Nachi · · 19 min read
Bloomreach to Textpattern Migration: A Technical Guide
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

What Changes When You Move from Bloomreach to Textpattern

A Bloomreach to Textpattern migration moves structured content from an enterprise headless CMS into a deliberately minimal PHP/MySQL publishing system. The architectural gap is severe. Bloomreach manages content through channels, document types, compound fields, and page layouts — all accessible via Management APIs and a Delivery API. Textpattern stores everything in a flat MySQL schema: articles in the textpattern table, image metadata in txp_image, files in txp_file, and categories in txp_category.

Textpattern has no write API and no import wizard. Every migration requires custom ETL scripts that extract from Bloomreach's export surfaces and load directly into MySQL.

Teams make this move to shed enterprise licensing costs, eliminate Java stack dependencies, or consolidate to a lightweight publishing tool where a small editorial team has direct database-level control. The trade-off is permanent: you lose multi-channel delivery, content personalization, structured document types, headless API delivery, and programmatic content workflows. You gain a fast, minimal system with zero external dependencies and full MySQL-level control over every piece of content.

Warning

No native migration path exists. Bloomreach exports content as NDJSON via its Content Batch Export API. Textpattern has no write API — data must be inserted directly into MySQL. Every migration requires custom extraction and loading scripts.

Estimated effort by site complexity:

Site profile Estimated effort
<100 documents, single channel, no custom compound types 2–3 days
100–1,000 documents, multiple document types, image assets 4–7 days
1,000+ documents, multiple channels, compound types, personalization variants, strict SEO requirements 1–3 weeks

These estimates assume one engineer with Python/SQL proficiency, a Textpattern staging instance running, and access to Bloomreach's Management APIs with a valid auth token. The dominant time cost at scale is not extraction — it's field mapping decisions and HTML cleanup on body content that contains unresolved internal references.

Which Bloomreach Product You're Migrating From

"Bloomreach" refers to distinct products with different export surfaces. Confusing them means writing the wrong extractor and misreading what "page" and "document" mean in the source system.

Bloomreach Content (SaaS) is a headless, API-first platform with Management APIs (Content Batch Export, Content Management, Content Type Management) and a read-only Delivery API. Content is organized into channels, which act as the root entity in the site configuration model. Its content model consists of:

  • Documents — structured content items defined by document types. Each document type defines its own schema with primitive fields (string, boolean, date) and compound fields (rich text, image links, content references).
  • Pages — layout entities containing component hierarchies that reference documents.
  • Resource bundles — key-value pairs for translations and configuration strings.
  • Folders — organizational containers. Metadata lives on documents, not folders.

Bloomreach Experience Manager (brXM) is the older self-hosted/PaaS deployment model built on a Java Content Repository (JCR). Published content is exposed through a Content REST API on the delivery tier, while repository-level exports use JCR console/XML tooling. (xmdocumentation.bloomreach.com)

Info

brXM users: The Content REST API does not have authentication by default. If your instance hasn't added custom auth, all published content is openly accessible. Verify this before assuming you need credentials.

Textpattern's Data Model: What You're Loading Into

Textpattern's schema is radically simpler than Bloomreach's. All content lives in MySQL tables with fixed columns. These constraints are not configuration choices — they are hard schema limits that determine what content survives the migration.

The textpattern Table (Articles)

This is where all your Bloomreach documents will land. Key columns:

Column Type Notes
ID INT Auto-increment primary key
Title VARCHAR(255) Article title
Body MEDIUMTEXT Raw body content (Textile, HTML, or plain text)
Body_html MEDIUMTEXT Pre-rendered HTML version — must not be left blank if textile_body=0
Excerpt TEXT Max 64KB
Excerpt_html MEDIUMTEXT Pre-rendered HTML of excerpt
Section VARCHAR(255) Exactly one section per article
Category1 VARCHAR(64) First category
Category2 VARCHAR(64) Second category — hard maximum of two
Status INT 1=draft, 2=hidden, 3=pending, 4=live, 5=sticky
textile_body VARCHAR(32) 0=raw HTML, 1=Textile, 2=convert line breaks
custom_1 through custom_10 VARCHAR(255) Only 10 custom fields, each capped at 255 characters
Keywords VARCHAR(255) Comma-separated, 255 char limit total
url_title VARCHAR(255) URL slug
AuthorID VARCHAR(64) Must match a name in txp_users — orphaned articles appear in admin but may break views
uid VARCHAR(32) Unique identifier, required for RSS/Atom feeds — missing causes silent feed breakage
feed_time DATE Required for RSS/Atom feeds — missing causes silent feed breakage

(docs.textpattern.com)

Hard Limits That Will Break Your Migration If Ignored

These are the constraints most often discovered late in a migration and are worth enumerating before writing a single line of transformation code:

  • Two categories maximum per article. Bloomreach documents with multiple taxonomy assignments lose all but two. There is no workaround within core Textpattern — categories 3 through N are simply dropped.
  • 10 custom fields, each VARCHAR(255). Bloomreach compound fields, nested objects, and long-form metadata don't fit. The glz_custom_fields plugin (maintained as of Textpattern 4.8.x) can extend this with unlimited fields of various data types including textarea, date, and select, but it introduces a plugin dependency that must be evaluated before commit.
  • No content references. Bloomreach's inter-document linking (content pickers, document references) has no equivalent. These must be flattened to plain text or hyperlinks during transformation, or they will appear as raw UUID strings in your article body.
  • Single section per article. Bloomreach documents can appear on multiple pages across channels. In Textpattern, each article belongs to exactly one section — a structural, not cosmetic, constraint.
  • 64KB page template limit. Each Textpattern page template can store at most 64KB of template code, which makes flattening a component-driven Bloomreach experience into a single template unworkable. (docs.textpattern.com)
  • Body_html must be populated when textile_body=0. If you insert raw HTML into Body and leave Body_html empty, the article renders blank on the frontend. This failure is silent and easy to miss until frontend QA.

Extracting Content from Bloomreach

SaaS: Content Batch Export API

The Content Batch Export API is the primary extraction tool for Bloomreach Content SaaS. Key facts:

  • It exports documents, pages, resource bundles, and folders from a specified folder path.
  • Output format is a zip file containing NDJSON (newline-delimited JSON) — one JSON object per line, per content item.
  • It's an asynchronous operation: you POST a request, receive an operationId, then poll for status until the export completes.
  • Images and assets are not included. You must export media separately via a distinct workflow.
  • The API supports a modifiedAfter timestamp for delta extraction — critical for cutover workflows where content continues to be authored during migration.
  • The API requires token authorization.

(documentation.bloomreach.com)

Here's the extraction flow:

# 1. Start the export job
curl -X POST \
  'https://{account}.bloomreach.io/management/content-export/v1/export' \
  -H 'Authorization: Bearer {token}' \
  -H 'Content-Type: application/json' \
  -d '{
    "sourcePath": "/content/documents/my-channel",
    "dataTypes": ["document", "page", "resourcebundle", "folder"]
  }'
 
# Response includes operationId
# {"operationId": "abc-123", "status": "STARTING", ...}
 
# 2. Poll for completion
curl -X GET \
  'https://{account}.bloomreach.io/management/content-export/v1/export/abc-123' \
  -H 'Authorization: Bearer {token}'
 
# 3. When status is COMPLETED, download the NDJSON zip
curl -X GET \
  'https://{account}.bloomreach.io/management/content-export/v1/export/abc-123/result' \
  -H 'Authorization: Bearer {token}' \
  -o export.zip

Use the Delivery API 2.0 alongside batch export for audits, count checks, and pre-cutover delta validation. It provides queryable access to documents with limit, offset, field filters, orderBy, and lastModified(gt) filtering. Folder traversal has a limit <= 100, so large repositories need explicit pagination. (documentation.bloomreach.com)

brXM (Self-Hosted): Content REST API

For brXM, the Content REST API exposes all published content based on document types. Paginate with _offset and _max parameters (default and maximum page size: 100):

import requests
 
base_url = "https://your-site.com/site/api/documents"
all_docs = []
offset = 0
page_size = 100
 
while True:
    resp = requests.get(base_url, params={
        "_offset": offset,
        "_max": page_size
    })
    items = resp.json().get("items", [])
    if not items:
        break
    all_docs.extend(items)
    offset += page_size

If the brXM site uses Experience Pages, Delivery API 1.0 is often the better extraction surface because page responses can serialize referenced documents, image sets, and assets together with the page model. Reach for JCR console export only when you need unpublished state, repository-only metadata, or an exact node hierarchy.

Pre-Migration Audit: Quantify Data Loss Before You Commit

Before writing transformation code, run a structured field inventory against your Bloomreach export. This step prevents discovering irreversible data loss after loading into staging.

For each document type, capture:

import json
from collections import defaultdict
 
field_usage = defaultdict(lambda: {"count": 0, "max_length": 0, "types": set()})
 
with open("export.ndjson") as f:
    for line in f:
        doc = json.loads(line)
        for field_name, field_value in doc.get("data", {}).items():
            field_usage[field_name]["count"] += 1
            if isinstance(field_value, dict):
                value_str = json.dumps(field_value)
            else:
                value_str = str(field_value)
            field_usage[field_name]["max_length"] = max(
                field_usage[field_name]["max_length"], len(value_str)
            )
            field_usage[field_name]["types"].add(type(field_value).__name__)
 
# Fields that exceed VARCHAR(255) — cannot fit in custom fields
for field, stats in sorted(field_usage.items(), key=lambda x: -x[1]["max_length"]):
    will_truncate = stats["max_length"] > 255
    print(f"{field}: used {stats['count']}x, max {stats['max_length']} chars, "
          f"truncates={'YES' if will_truncate else 'no'}")

This gives you a count of fields that exceed Textpattern's 255-character custom field limit, fields with complex types (nested objects, arrays) that cannot survive flattening, and fields used infrequently enough to drop without editorial impact. Running this audit before stakeholder sign-off converts "we'll figure it out" into a documented, accepted data loss table.

Exporting Images Separately

Since the Batch Export API does not include images and assets, you have three options:

  1. External DAM: If Bloomreach references images from a DAM (Cloudinary, Bynder, etc.), those URLs remain valid post-migration. Update references in content bodies accordingly.
  2. Delivery API scraping: Fetch published content via the Delivery API and parse image URLs from the responses. Resource URLs can point to original files or image variants.
  3. Manual/JCR export: For brXM, images are stored in the JCR repository. Use WebDAV access or the Hippo Content EXIM tooling to bulk-export binaries.

Be aware of Bloomreach Content's media constraints: images are public by default with a 4MB and 1920×1280 upload limit; assets are public by default with a 10MB limit. Some source originals may already be constrained or transformed before export. (documentation.bloomreach.com)

Handling Rich Text References

Bloomreach stores rich text as HTML, but embedded images and internal links are often represented as reference nodes (e.g., hippogallery:imageset or internal UUIDs in brXM; structured reference objects in Content SaaS) rather than standard <img> or <a> tags. During extraction, your script must parse the JSON, locate these internal references, and resolve them against the linked objects in the API response. If you skip this step, your Textpattern articles will contain broken UUID strings instead of actual images and links. This is the most common source of body content corruption in this migration path.

Transforming Bloomreach Content for Textpattern's Schema

Document-to-Article Mapping

Map Bloomreach document types to Textpattern Sections. Textpattern relies on Sections to define URL structures and page templates. If your Bloomreach setup has multiple channels (e.g., en-us and fr-fr), map each to a Textpattern section — or use separate Textpattern instances for true multi-site.

Use Textpattern's two available Category slots (Category1, Category2) for secondary classification and the Keywords field for additional tagging.

Here's a Python transformation function:

def transform_bloomreach_doc(doc, section_map, category_map):
    """Transform a Bloomreach NDJSON document to a Textpattern article dict."""
    fields = doc.get("data", {})
    doc_type = doc.get("type", "")
    
    # Map Bloomreach document type to Textpattern section
    section = section_map.get(doc_type, "articles")
    
    # Extract rich text body — Bloomreach stores HTML in rich text fields
    body_html = fields.get("body", {}).get("value", "")
    
    # Handle categories (max 2 in Textpattern)
    categories = fields.get("categories", [])
    cat1 = category_map.get(categories[0], "") if len(categories) > 0 else ""
    cat2 = category_map.get(categories[1], "") if len(categories) > 1 else ""
    
    # Warn on data loss
    if len(categories) > 2:
        print(f"WARNING: {fields.get('title', 'unknown')} has {len(categories)} categories; "
              f"only first 2 will be migrated")
    
    return {
        "Title": fields.get("title", "")[:255],
        "Body": body_html,
        "Body_html": body_html,  # must match Body when textile_body=0
        "Excerpt": fields.get("introduction", {}).get("value", "")[:65535],
        "Excerpt_html": fields.get("introduction", {}).get("value", "")[:65535],
        "textile_body": 0,  # 0 = raw HTML, no processing
        "textile_excerpt": 0,
        "Section": section,
        "Category1": cat1[:64],
        "Category2": cat2[:64],
        "Status": 4,  # live
        "Posted": fields.get("publishDate", ""),
        "url_title": generate_url_slug(fields.get("title", "")),
        "Keywords": ",".join(fields.get("tags", []))[:255],
        "AuthorID": fields.get("author", "admin"),
    }

Flattening Multiple Content Fields

If a Bloomreach document contains multiple rich text fields (e.g., introText, mainBody, conclusion), concatenate them into a single HTML string for Textpattern's Body column. Bloomreach's component-based page composition, personalization variants, and dynamic content injection do not survive — you are migrating raw text, images, and metadata only.

Warning

The core constraint: You cannot migrate Bloomreach's presentation layer. Component layouts, personalization variants, and dynamic content injection are permanently lost. Everything flattens into a single body field.

The Textile Decision

Textpattern natively supports Textile, a lightweight markup language. You have three options for body content:

Option textile_body value When to use
Store as raw HTML 0 Default choice. Bloomreach rich text is already HTML; set Body and Body_html to the same value. Zero conversion risk.
Convert to Textile 1 Only if your editorial team specifically wants Textile authoring going forward. Use Pandoc for conversion; audit tables, nested lists, and footnotes manually — these do not round-trip cleanly.
Store both 0 (serve HTML) Put original HTML in Body_html, a Textile approximation in Body. Protects against lossy conversion while enabling future Textile editing.

Decision rule: Use raw HTML (option 1) unless your editorial team has explicitly requested Textile. Converting to Textile adds engineering risk and requires manual QA on every complex HTML structure without providing any end-user benefit unless editors plan to write in Textile going forward.

Custom Field Mapping

This is where data loss becomes quantifiable. A Bloomreach document type with 15 fields cannot fit into Textpattern's 10 custom_1 through custom_10 fields, each capped at 255 characters.

Mapping strategy (in priority order):

  1. Run the pre-migration audit script (see above) to identify which fields are actually populated and how large their values are.
  2. Map the top 10 most editorially active fields to custom_1 through custom_10. Fields used in fewer than 5% of documents are candidates for dropping.
  3. For values that exceed 255 characters, either truncate with documented business acceptance, or serialize into a JSON blob embedded in the article body as a data attribute — not elegant, but recoverable.
  4. For production deployments, evaluate glz_custom_fields (Textpattern plugin, maintained for 4.8.x as of this writing). It extends custom fields with unlimited fields of types including textarea, date, radio, checkbox, and select. This plugin introduces a dependency you must track for upgrades.
  5. Accept that compound fields (nested objects, arrays of references) cannot be represented in Textpattern's schema and must be flattened or discarded. Document every such decision in a migration log.

Channel and Page Mapping

Bloomreach's concept of channels and pages (layout entities with component hierarchies) has no direct Textpattern equivalent:

  • Channels → Sections: Map each Bloomreach channel to a Textpattern section. Textpattern uses a lateral structure rather than a hierarchical one (a constraint we explore further in our Textpattern to WordPress migration guide).
  • Pages → Page templates: Bloomreach pages with component layouts map to Textpattern's txp_page templates and txp_form forms. This is a manual rebuild — there is no way to automate component-to-tag conversion.
  • Menus → Links or hardcoded navigation: Bloomreach menus don't export to a portable format. Rebuild navigation using Textpattern's txp_link table or section-based navigation tags.
  • Resource bundles → Custom strategy: Bloomreach resource bundles (i18n key-value pairs) rarely belong in articles. Textpattern's core publishing model has no equivalent for message-level localization. The txp_lang table stores interface strings only — it is not a content translation system.

Loading Data Into Textpattern

Preparing the Database

Before inserting articles, create the required relational data in this order. Loading articles first causes orphaned records and silent failures.

  1. Users: Create accounts in txp_users. The name column (login ID) is what maps to AuthorID in the articles table — not RealName, not email.
  2. Sections: Create sections in txp_section.
  3. Categories: Create categories in txp_category. Textpattern uses the modified preorder tree traversal (MPTT) algorithm for category hierarchy — you must set lft and rgt values correctly or queries against the category tree will return wrong results silently.
-- Create sections
INSERT INTO txp_section (name, title, page, css, skin, in_rss, on_frontpage, searchable)
VALUES ('blog', 'Blog', 'default', 'default', 'default', 1, 1, 1);
 
-- Create categories (type must be 'article', 'image', 'file', or 'link')
INSERT INTO txp_category (name, type, parent, lft, rgt, title)
VALUES ('news', 'article', 'root', 2, 3, 'News');

After inserting all categories, run the Textpattern Diagnostics panel — it will detect and offer to repair MPTT tree inconsistencies.

The most reliable approach. Generate SQL statements from your transformed data:

INSERT INTO textpattern (
  Title, Body, Body_html, Excerpt, Excerpt_html,
  Section, Category1, Category2, Status,
  textile_body, textile_excerpt, Posted, LastMod,
  url_title, AuthorID, uid, feed_time, Keywords
) VALUES (
  'My Article Title',
  '<p>Article body HTML</p>',
  '<p>Article body HTML</p>',
  'Short excerpt',
  '<p>Short excerpt</p>',
  'blog', 'news', '',
  4,  -- live
  0,  -- raw HTML
  0,  -- raw HTML
  '2025-03-15 10:00:00',
  '2025-03-15 10:00:00',
  'my-article-title',
  'admin',
  MD5(CONCAT(RAND(), NOW())),  -- generates unique ID; not an official Textpattern method
  '2025-03-15',
  'bloomreach,migration'
);

Note: MD5(CONCAT(RAND(), NOW())) for uid generation is a common workaround but is not documented as an official Textpattern recommendation. Textpattern's own code generates UIDs using PHP's md5(uniqid(rand(), true)). Either approach produces a sufficiently unique value for feed purposes; the key requirement is that uid is unique per article and non-null.

Danger

Critical database quirks:

  • When textile_body=0, populate both Body and Body_html with the same HTML payload. Leaving Body_html blank causes the article to render empty on the frontend.
  • uid and feed_time are required for RSS/Atom feeds. Missing values cause silent feed breakage — not a PHP error, not a warning, just missing items.
  • AuthorID must exactly match a name value in txp_users. Articles with non-existent author references appear in the database but may break admin views and author-filtered queries.
Warning

Category tree integrity: If you insert categories manually, set lft and rgt values correctly. After inserting all categories, run the Textpattern admin Diagnostics panel to detect and repair MPTT tree inconsistencies before loading articles.

Image and File Migration

Textpattern stores image and file metadata in txp_image and txp_file tables, but the actual binaries live on disk at specific paths that are tied to the auto-increment database ID:

  • Images: /images/{id}.{ext} (thumbnails: /images/{id}t.{ext})
  • Files: /files/{filename}

This ID-to-filename coupling is not optional. If the file is not at /images/{id}.jpg, Textpattern image tags fail silently.

The migration flow for every image:

  1. Download the binary from the Bloomreach CDN URL or DAM.
  2. Insert metadata into txp_image to generate the auto-increment ID:
INSERT INTO txp_image (
    name, category, author, alt, caption, date, ext, w, h
) VALUES (
    'original_filename.jpg',
    'general',
    'admin',
    'Alt text from Bloomreach',
    'Caption from Bloomreach',
    '2025-03-15 10:00:00',
    '.jpg',
    800,
    600
);
-- Note the generated ID: LAST_INSERT_ID()
  1. Rename and copy the file to /images/{id}.{ext} using the assigned database ID (e.g., ID 42/images/42.jpg).
  2. Update article body HTML to replace Bloomreach image URLs with Textpattern paths (/images/42.jpg) or Textpattern image tags (<txp:image id="42" />).

For guidance on handling media across platforms, see How to Migrate Images, Attachments & Embeds Without Broken Links.

URL Structure and SEO Redirect Map

Bloomreach URLs are typically channel-routed (e.g., /blog/2025/my-post) and defined by the site's routing configuration. Textpattern uses section-based permalinks with several possible modes:

  • /section/id/title (default)
  • /section/title
  • /year/month/day/title
  • /title-only

The URL structures will change. Every Bloomreach URL with organic search value needs a 301 redirect to its Textpattern equivalent.

Redirect generation process:

  1. Extract the full URL path from Bloomreach's routing configuration or sitemap.
  2. Determine the new Textpattern URL based on your chosen permlink mode.
  3. Generate redirect rules as part of your transformation script — not manually, not after cutover.
# Nginx rewrite
rewrite ^/news/press-releases/2023/q4-earnings/?$ /press/q4-earnings permanent;
# Apache .htaccess
Redirect 301 /blog/2025/03/enterprise-content-strategy /blog/enterprise-content-strategy

For sites with thousands of URLs, avoid storing all redirects in .htaccess — the file is read on every request and degrades performance at scale. Use a RewriteMap in Apache or a key-value store in Nginx to handle redirects at the server level. Every unmapped URL with search value is a permanent ranking loss.

What You Permanently Lose

Bloomreach capability Textpattern equivalent Impact
Custom document types with typed fields Flat article model + 10 custom fields (255 chars each) Major data loss for complex types
Rich text with embedded content references HTML or Textile body text Internal doc links become broken strings
Multi-channel delivery Single site per install Must duplicate for multi-site
Personalization / targeting None Complete feature loss
Headless API (Delivery API) No content API Frontend apps lose data source
Workflow (draft → review → publish) Basic status flags (1=draft, 3=pending, 4=live) Simplified editorial process
Compound / nested fields VARCHAR(255) custom fields Cannot represent complex data
Resource bundles (i18n) txp_lang interface strings only Not a content translation system
Image/asset management via API File-based with DB metadata, ID-coupled filenames Manual binary migration required
Component-based page layouts Tag-based templates with 64KB limit Full rebuild required
More than 2 taxonomy terms per content item Category1, Category2, Keywords varchar Taxonomy collapses significantly

Common Failure Modes

1. Body content with broken references. Bloomreach rich text contains inline content references, embedded components, and dynamic content blocks that render as broken markup or raw UUID strings in Textpattern. Audit and clean HTML before inserting — after insertion, finding and fixing these at scale requires touching every affected article row.

2. Category tree corruption. Inserting categories without correct lft/rgt MPTT values breaks Textpattern's tree traversal. Symptoms: categories appear in admin but return no articles; category parent/child relationships display incorrectly. Fix via Diagnostics panel post-import.

3. Author mismatch. AuthorID must match a name in txp_users. Articles with non-existent author references appear in the database but become orphaned in admin views and break author-filtered template tags. Create all user accounts before loading articles.

4. Missing uid and feed_time. These columns are required for RSS/Atom feeds. Omitting them causes silent feed breakage — items simply don't appear in feeds without any error output. Generate uid with a unique hash function and set feed_time to the article creation date on every inserted row.

5. Body_html left blank. When textile_body=0, Textpattern serves Body_html directly. Leaving it null or empty causes articles to render blank on the frontend. This fails silently — no error, just empty content blocks.

6. Image path references not rewritten. Bloomreach image URLs won't resolve after migration. Run a find-and-replace pass across all Body_html and Excerpt_html columns to rewrite paths to /images/{id}.{ext} format before going live.

7. Loading to production before staging validation. Direct database access removes guardrails. Always load into staging first. Verify article counts, media presence, body rendering, category tree integrity, redirect coverage, and RSS feed output before promoting or replaying the import to production.

When This Migration Does Not Make Sense

This migration works for teams intentionally downsizing from enterprise CMS complexity to a minimal publishing tool. It does not make sense if any of the following are true:

  • You deliver content to mobile apps or SPAs via API. Textpattern has no content delivery API. This is a hard stop, not a limitation to work around.
  • Your content model uses more than 10 distinct metadata fields per document type. The glz_custom_fields plugin removes this ceiling, but it's a plugin dependency with upgrade risk.
  • You need multi-language content management. Textpattern's i18n covers interface strings, not content translation workflows. There is no built-in equivalent to Bloomreach's channel-level locale management.
  • Your editorial team requires approval workflows. Textpattern's draft/pending/live status flags are a significant downgrade from Bloomreach's project-based workflow with reviewer assignments.
  • You manage more than one site from a single CMS installation. Textpattern is one-site-per-installation. Multi-site requires separate installs with separate databases.
  • You need more than two first-class taxonomy terms per content item. The two-category limit is a schema constraint, not a configuration option.

If you are moving away from Bloomreach but need structured content with API delivery, choose a different target (such as an enterprise DXP like Sitecore). Textpattern is the correct choice only when intentional simplification — not feature parity — is the explicit goal.

Migration Checklist

  1. Identify the source Bloomreach product. Confirm Content SaaS or brXM — the export surfaces differ and the wrong choice means rewriting your extractor.
  2. Run the pre-migration field audit. Count document types, enumerate all fields, measure max field lengths, identify compound types. Produce a documented data loss table before stakeholder sign-off.
  3. Map document types to sections. Each Bloomreach document type or channel becomes a Textpattern section.
  4. Export via Batch Export API (SaaS) or Content REST API (brXM). Verify document count against Delivery API totals.
  5. Export images separately. DAM URLs, Delivery API scraping, or JCR WebDAV export — document which method and verify binary count.
  6. Build transformation scripts. Parse NDJSON/JSON, map fields to Textpattern columns, handle HTML cleanup, resolve internal content references, and enforce truncation at schema limits.
  7. Set up Textpattern staging. Fresh install. Create users, sections, and categories (with correct lft/rgt MPTT values) before loading articles.
  8. Load articles via SQL INSERT. Validate row count matches source document count.
  9. Migrate image binaries to /images/{id}.{ext} directory and insert txp_image metadata. Verify ID-to-filename coupling.
  10. Rewrite image references in Body_html and Excerpt_html to Textpattern paths.
  11. Generate and deploy 301 redirects. Every Bloomreach URL with search value needs a mapped redirect rule.
  12. Run delta sync using modifiedAfter (SaaS) to capture content changes made after the base export.
  13. Run Textpattern Diagnostics panel. Check for MPTT tree corruption, missing author references, and other detected issues.
  14. Test every content type on the frontend: article rendering, category filtering, author pages, RSS feeds, image display, and redirect behavior.

For a structured approach to zero-downtime migration planning, see The Ultimate Knowledge Base Migration Checklist.

The Practical Path Forward

A Bloomreach-to-Textpattern migration succeeds when you separate content extraction, media re-hosting, template rebuild, and URL cutover into distinct workstreams with explicit acceptance criteria. Treating it as a vendor export followed by a weekend import is the most reliable way to arrive at production with broken body content, missing images, and orphaned articles.

The pre-migration field audit is the highest-leverage investment. It converts vague data loss risk into a documented, stakeholder-accepted table of decisions — which fields survive, which get truncated, which get dropped. That table is the contract between engineering and editorial before a single INSERT runs.

This is an exercise in intentional simplification. By stripping away the headless architecture, you regain direct database-level control, lower your hosting footprint, and eliminate the need for a dedicated frontend engineering team. The migration is technically tractable. What typically goes wrong is not the SQL — it's the undiscovered content model complexity that surfaces during transformation.

Frequently Asked Questions

Can you migrate Bloomreach content to Textpattern automatically?
No. There is no native migration path, connector, or import wizard. Bloomreach exports content as NDJSON via its Batch Export API, but Textpattern has no write API. You must build custom scripts to transform Bloomreach's structured documents into SQL INSERT statements for Textpattern's MySQL tables.
Does Bloomreach's batch export API include images?
No. The Bloomreach Content Batch Export API does not support images and assets. If you store media in Bloomreach's built-in repository rather than an external DAM, you must export images separately via Delivery API scraping, JCR WebDAV access, or manual download.
How many custom fields does Textpattern support?
Textpattern supports 10 custom fields by default (custom_1 through custom_10), each limited to VARCHAR(255). If your Bloomreach document types have more than 10 metadata fields, you need the glz_custom_fields plugin or must serialize overflow data into the article body.
Should I convert Bloomreach HTML to Textile for Textpattern?
For most migrations, no. Store Bloomreach's HTML directly in Textpattern with textile_body set to 0 (raw mode). Converting to Textile via Pandoc introduces risk — complex tables, nested lists, and embedded content may not convert cleanly. Only convert if your team specifically wants Textile authoring going forward.
What data is permanently lost in a Bloomreach to Textpattern migration?
You permanently lose multi-channel delivery, content personalization, headless API access, compound/nested field structures, project-based editorial workflows, content reference linking between documents, and resource bundle i18n support. Textpattern's flat model cannot represent Bloomreach's structured content architecture.

More from our Blog