52 Blog Posts, Claude, 3 Prompts, Under an Hour

Published March 10, 2026

portrait of Alexis Roberson.

by Alexis Roberson

So, how did I turn a week-long blog migration into a one-hour task? The short answer is claude and also providing prompts containing more details than a flight safety manual.

When LaunchDarkly acquired Highlight in March 2025, one of the first tasks on the integration roadmap was straightforward on paper: migrate Highlight’s developer-focused blog content into LaunchDarkly’s documentation site. Highlight had accumulated over 100 blog posts covering observability, OpenTelemetry, session replay, ClickHouse optimization, and more — the kind of deep technical content that aligned naturally with LaunchDarkly’s tutorials page.

The reality was more nuanced. Not every blog was being transferred. The source files were in Markdown, but LaunchDarkly’s docs are powered by Fern and require .mdx with specific frontmatter conventions. The blogs needed to be integrated in chronological order alongside existing tutorials. Author images needed to follow a naming convention. Brand references and URLs had to be updated. And all of this needed to be done carefully — on a production documentation repository that serves real users.

I completed the entire migration in under an hour. Here’s how.

The scope: 52 blogs, two destinations

The observability team identified the blogs to migrate using a shared Excel spreadsheet, marking each approved post with a checkmark. Out of 107 total posts in Highlight’s blog-content repository, 52 made the cut.

These 52 posts were split between two destinations on LaunchDarkly’s site:

  • 21 posts went to the Tutorials page — practical guides on topics like OpenTelemetry instrumentation, distributed tracing, and monitoring frameworks
  • 31 posts went to the Flagship blog — engineering deep dives on ClickHouse optimization, session replay benchmarking, error grouping, and infrastructure work

The split was intentional. Tutorial-style content like “The Complete Guide to OpenTelemetry in Python” or “How to Instrument Your React Native App with OpenTelemetry” belonged in tutorials. Engineering narratives like “Migrating from OpenSearch to ClickHouse” or “Building GitHub Enhanced Stacktraces” fit the blog’s voice better.

Step 1: Filter and stage the approved blogs

The first step was extracting only the approved posts from Highlight’s full blog repository. I downloaded the Excel sheet containing the approval checkmarks and the blog-content directory from Highlight’s GitHub repo. My initial prompt to Claude laid out the filtering criteria:

Clone launchdarkly/observability/blog-content and highlight/highlight/blog-content and compare them to make sure they’re matching. If they are matching, filter the blogs that contain “blogs” in the path and that do not contain “vs” or “VS” or “Vs” in the blog title. For every blog left over, convert the .md file to .mdx to match fern standards, keep the dates the same. Once converted, migrate every .mdx file into the ld-docs-private repo under tutorials and ensure chronological order by referencing the date in the file.

This first prompt established the end-to-end vision. From there, I broke the execution into discrete steps, starting with creating a new blogs-to-migrate/blog-content directory containing only the 52 approved posts.

This gave me a clean working directory with exactly the files I needed, separated from the full 107-post source repository.

Step 2: Convert .md to .mdx with a single prompt

LaunchDarkly’s Fern documentation requires .mdx files with a specific frontmatter structure that looks nothing like Highlight’s original format. Here’s what a Highlight blog’s frontmatter looked like:

1title: "What is Frontend Monitoring and What Tools Help You Do It?"
2createdAt: 2022-08-23T12:00:00Z
3readingTime: 8
4authorFirstName: "Haroon"
5authorLastName: "Choudery"
6authorTitle: "Software Engineer"
7tags: "Frontend, Observability"

And here’s what Fern needed:

1slug: /tutorials/what-is-frontend-monitoring
2title: "What is Frontend Monitoring and What Tools Help You Do It?"
3og:title: "What is Frontend Monitoring and What Tools Help You Do It?"
4description: "For frontend developers, building cool apps for the web is fun."
5og:description: "For frontend developers, building cool apps for the web is fun."
6keywords: frontend, observability, monitoring, tools

Plus a structured author block with image references, a formatted publish date, and content transformations like wrapping images in <Frame> components and converting internal blog links to relative /tutorials/ paths.

I provided Claude with a single example of the target .mdx format and a detailed prompt that covered every transformation rule in one shot. Here’s the prompt I used:

Now here comes the fun part. I’m converting all the files in blogs-to-migrate/blog-content into .mdx files for fern documentation. Can you use this example blog structure as a reference:

---
slug: /tutorials/detection-to-resolution-rage-clicks
title: "Detection to Resolution: Real world debugging with rage clicks and session replay"
og:title: "Detection to Resolution: Real world debugging with rage clicks and session replay"
description: "Learn how to use session replay and rage click detection to diagnose and fix production issues fast through three real-world case studies."
og:description: "Learn how to use session replay and rage click detection to diagnose and fix production issues fast through three real-world case studies."
keywords: monitoring, observability, rage clicks, debugging
---
<p class="publishedDate"><em>Published February 11, 2026</em></p>
<div class="authorWrapper">
<img
src="../../../assets/images/authors/alexis-roberson.png"
alt="portrait of Alexis Roberson."
class="authorAvatar"
/>
<p class="authorName">by Alexis Roberson</p>
</div>

For each blog, the slug should be set to tutorials/, for example, for the blog whose file name is what-is-frontend-monitoring, the slug should be /tutorials/what-is-frontend-monitoring. The title can be the original title field in the .md file. Don’t forget to set a og:title field with the same title for each new .mdx file. For the description and og:description field, you will need to generate a one sentence description for each blog, and you will also need to generate 4 keywords for each blog post that are related to the blog. For example keywords: monitoring, observability, rage clicks, debugging. For the published date make sure its in this format Month Date, Year. So like this February 11, 2026. For the div img src tag, the name of the .png file should be the first and last name of the author. So if the person’s name is Alexis Roberson, the src should be src=”../../../assets/images/authors/alexis-roberson.png” and the div img alt should be set to for example portrait of Alexis Roberson.

Notice how much detail is packed into this single prompt — the example template, the slug convention, the date format, the author image path pattern, and instructions for generating descriptions and keywords. This level of specificity is what allowed Claude to generate a complete, correct script on the first pass.

Claude generated convert_blogs.py — a 277-line Python script that handled all 52 conversions in one execution. The script:

  • Parsed YAML frontmatter including multi-line values and various quoting styles
  • Generated descriptions by extracting the first meaningful paragraph, stripping markdown formatting, and capping at 160 characters
  • Extracted keywords by combining the original tags with significant title words (filtering out stop words), ensuring exactly four keywords per post
  • Formatted dates from ISO 8601 to “Month Day, Year” format
  • Transformed body content by removing <BlogCallToAction /> components, wrapping standalone images in <Frame> tags, and converting internal Highlight blog links to relative /tutorials/ paths

Here’s the transform_body() function that handled the content transformations — the most nuanced part of the conversion:

1def transform_body(body):
2 """Apply body content transformations."""
3 lines = body.split("\n")
4 result = []
5 in_code_block = False
6 i = 0
7
8 while i < len(lines):
9 line = lines[i]
10
11 # Track code blocks to avoid transforming inside them
12 if line.strip().startswith("```"):
13 in_code_block = not in_code_block
14 result.append(line)
15 i += 1
16 continue
17
18 if in_code_block:
19 result.append(line)
20 i += 1
21 continue
22
23 # Remove <BlogCallToAction /> lines (and surrounding blank lines)
24 if re.match(r'^\s*<BlogCallToAction\s*/?\s*>\s*$', line):
25 if i + 1 < len(lines) and lines[i + 1].strip() == "":
26 i += 2
27 else:
28 i += 1
29 if result and result[-1].strip() == "":
30 result.pop()
31 continue
32
33 # Wrap standalone images in <Frame> tags
34 img_match = re.match(r'^(\s*)(!\[.*\]\(.*\))\s*$', line)
35 if img_match:
36 indent = img_match.group(1)
37 img_tag = img_match.group(2)
38 result.append(f"{indent}<Frame>")
39 result.append(f"{indent} {img_tag}")
40 result.append(f"{indent}</Frame>")
41 i += 1
42 continue
43
44 # Update internal blog links
45 line = re.sub(
46 r'https://www\.highlight\.io/blog/([a-zA-Z0-9_-]+)',
47 r'/tutorials/\1',
48 line
49 )
50
51 result.append(line)
52 i += 1
53
54 return "\n".join(result)

This function walks through every line of the blog body, skipping code blocks, stripping Highlight-specific call-to-action components, wrapping images in Fern’s <Frame> component, and rewriting internal blog links — all in a single pass.

One execution. 52 files converted. Zero manual edits needed.

Step 3: Rebrand content while preserving code integrity

The converted blogs still referenced “Highlight” throughout their content. A naive find-and-replace would have been destructive — these are technical posts filled with code blocks referencing HighlightInit, API endpoints like otel.highlight.io, and package imports. Replacing those would break every code example.

Claude generated replace_brand.py, a script with a surgical approach to brand replacement. It used regex-based protection to identify and preserve:

  • Code blocks (anything between ``` delimiters)
  • Inline code (backtick-wrapped text)
  • URLs (bare URLs and markdown link targets)
  • HTML attributes (src and href values)

Only prose text outside these protected segments got the brand swap. Here’s the core of that approach:

1# Regex that matches segments to PROTECT from brand replacement
2PROTECTED = re.compile(r"""
3 `[^`]+` # inline code
4 | \]\([^)]+\) # markdown link URL ](url)
5 | (?:src|href)="[^"]*" # HTML src/href attributes
6 | https?://\S+ # bare URLs
7""", re.VERBOSE)
8
9def replace_brand_in_segment(text):
10 """Replace Highlight brand references in an unprotected text segment."""
11 text = re.sub(r'(?<!\.)Highlight\.io', 'LaunchDarkly', text)
12 text = re.sub(r'(?<!\.)highlight\.io', 'LaunchDarkly', text)
13 text = re.sub(r'\bHighlight\b', 'LaunchDarkly', text)
14 return text
15
16def process_line(line):
17 """Process a single line, protecting URLs/code/paths."""
18 parts = []
19 last_end = 0
20 # cspell:disable-next-line
21 for match in PROTECTED.finditer(line):
22 # Unprotected text before this match — apply brand replacement
23 parts.append(replace_brand_in_segment(line[last_end:match.start()]))
24 # Protected match — keep as-is
25 parts.append(match.group())
26 last_end = match.end()
27 parts.append(replace_brand_in_segment(line[last_end:]))
28 return ''.join(parts)

The PROTECTED regex identifies every segment that should be left alone — inline code, link URLs, HTML attributes, and bare URLs. The process_line() function splits each line around those protected segments, only applying the brand swap to the gaps in between. The \b word boundary in replace_brand_in_segment() ensures compound identifiers like HighlightInit or AppRouterHighlight are never touched. The script processed all 52 files, updating only the lines that needed changes.

A follow-up script, update_urls.py, handled the URL layer — converting highlight.io/docs/ to launchdarkly.com/docs/, app.highlight.io to app.launchdarkly.com, and catching any remaining blog cross-references. It even handled edge cases like brand references inside ```hint blocks (which the previous script had conservatively skipped as code) and _Highlight_ markdown italic patterns.

Step 4: Transfer to the Fern repo in chronological order

With 52 properly formatted and rebranded .mdx files ready, the next step was integrating them into LaunchDarkly’s documentation repository. This wasn’t just copying files — each blog needed its own directory, a YAML config entry, and chronological ordering. Here’s the prompt I used:

I copied over the fern directory project called ld-docs-private to practice before the official migration. I want you to transfer all the .mdx files from blogs-to-migrate/blog-content and store them in ld-docs-private/fern/topics/tutorials, creating a folder for each blog and storing it in that folder. For example, if the blog .mdx file is what-is-feature-monitoring.mdx, then its path would be ld-docs-private/fern/topics/tutorials/what-is-feature-monitoring/what-is-feature-monitoring.mdx. Inside ld-docs-private/fern/topics/versions/ld-docs.yml file the blog title and the blog file folder and path created in the ld-docs-private/fern/topics/tutorials directory need to be added under tab:tutorials -> section: Tutorials -> contents where page is the title and path is the path to find the blog’s .mdx file. But here’s the catch — the blogs I’m migrating over to ld-docs-private need to be integrated in chronological order in respect to the existing blog order inside ld-docs.yml file based on the blog’s publish date.

This prompt specified three things at once: the directory structure convention, the YAML config format, and the chronological ordering constraint. That last requirement — “in respect to the existing blog order” — was the trickiest part.

Claude generated transfer_blogs.py to handle this. The heart of the script was the chronological merge — combining existing tutorials with the new Highlight posts and sorting them by publish date:

1def main():
2 # Parse existing tutorial entries from ld-docs.yml
3 existing = get_existing_entries()
4
5 # Collect new blog entries with their dates and titles
6 new_blogs = get_new_blog_entries()
7
8 # Create folder structure and copy .mdx files
9 create_tutorial_folders_and_copy(new_blogs)
10
11 # Merge all entries and sort by date, newest first
12 all_entries = existing + new_blogs
13 epoch = datetime(1970, 1, 1)
14 all_entries.sort(key=lambda e: e['date'] or epoch, reverse=True)
15
16 # Rewrite the tutorials section in ld-docs.yml
17 update_yml(all_entries)

The chronological ordering was critical. Most of the Highlight blogs were published between 2022 and 2024 — older than the latest LaunchDarkly tutorials. Simply appending them would have buried them at the bottom. The sort ensured they appeared at the correct position in the timeline, interleaved with existing content based on their original publish dates.

Step 5: Author images and asset management

Every blog post referenced an author portrait image following the firstname-lastname.png convention. I needed headshots for all the Highlight authors whose posts were being migrated. After collecting the images, I ran one more prompt:

Now can you take the authors from every blog I migrated over and add a png file for it with the name of the .png file being firstname-lastname.png, for instance alexis-roberson.png. Also make sure the name doesn’t already exist in the directory. The directory path is ld-docs-private/fern/assets/images/authors.

This ensured every author referenced in the .mdx files had a corresponding image in the right location, with no naming conflicts against existing author images.

The blog posts also referenced inline images stored under topic-specific directories in Fern’s assets. These image files and their directory structures needed to be copied over to ensure nothing rendered as a broken link.

The test-first approach

Before touching the production repository, I tested the entire workflow on a copy. I cloned the ld-docs-private repo into a local test directory and ran all four scripts against it. This let me verify that:

  • Every .mdx file rendered correctly with valid frontmatter
  • The ld-docs.yml configuration was valid YAML with proper indentation
  • Chronological ordering was correct
  • No brand references leaked through in code blocks
  • All internal links pointed to valid tutorial slugs
  • Author image paths resolved correctly

Only after confirming the test migration was clean did I run the same prompts against the real Fern repository.

What made this work

Looking back, the key to completing this in under an hour came down to three things:

Detailed first prompts. Each prompt I wrote specified every transformation rule, edge case, and output format upfront. I included a concrete example of the target .mdx structure, spelled out the slug derivation logic, the date format, the author image path convention, and the chronological ordering requirement. This front-loaded effort meant Claude could generate complete, correct scripts on the first pass instead of requiring iterative debugging.

Letting AI handle the tedious parts. Generating 52 unique descriptions from first paragraphs, extracting keywords, reformatting dates, handling YAML quoting edge cases, protecting code blocks during brand replacement — these are exactly the kinds of repetitive, detail-oriented tasks where manual work introduces errors and AI excels.

Testing on a copy first. Running the full pipeline on a cloned repo before touching production meant I could refine my prompts with zero risk. The few adjustments I made — like handling ````hint` blocks and ordinal date suffixes — were caught during testing, not after deployment.

The numbers

MetricCount
Total blogs migrated52
Posts to Tutorials page21
Posts to Flagship blog31
Python scripts generated4
Total lines of automation code~650
Manual file edits required0
Time to complete< 1 hour

Conclusion

Content migration is one of those tasks that looks simple until you start accounting for format differences, ordering constraints, brand consistency, and asset management. What could have been days of manual copy-paste-edit work became a streamlined, automated pipeline — powered by a few well-crafted prompts and the scripts they produced.

The Highlight blog content now lives on LaunchDarkly’s Tutorials and Blog pages, interleaved chronologically with existing content, properly branded, and fully integrated into the Fern documentation framework. If you’re facing a similar migration challenge, the approach is repeatable: define your target format precisely, test on a copy, and let automation handle the volume.