Home / Solutions / Fix Orphaned Pages With XML and WP CLI
Solutions

Fix Orphaned Pages With XML and WP CLI

Add relevant internal links to orphaned pages in an XML export, then update posts with WP-CLI while keeping their URLs and dates. See the method.

Fix Orphaned Pages With XML and WP CLI

The problem

Some websites have many orphaned pages. These are pages with no internal link pointing to them. Fixing them by hand takes a long time, and hiring a team costs money.

There is another catch. If you edit an exported XML file and import it with the normal WordPress importer, existing posts are not updated. That is why I use a small WP-CLI script.

Step 1: add the links in the XML

  1. Export your posts from WordPress as an XML file.
  2. Give the XML to an AI, together with your link rules and a list of approved URLs.
  3. Ask the AI to change only the text inside the content field. IDs, titles, slugs, dates and categories must stay the same.
  4. Keep the old text, images, shortcodes and links.

The rules I give the AI are simple. Put one article link in the first three paragraphs. Put a second link, to a product or a page, in the paragraph just before the FAQ. Use natural anchor text, one link per paragraph, and root-relative paths like /your-post-slug/. Every post must get at least one incoming link.

Step 2: update the posts with WP-CLI

  1. Take a full backup first.
  2. Upload the script and the XML file into the WordPress root, the folder with wp-config.php.
  3. Turn on SSH in your hosting panel and connect with the command it shows.
  4. Go to your WordPress folder with cd and check that wp-config.php is there with ls.
  5. Run the preview command below. It saves nothing.
  6. Read the summary. If many posts say SKIP, the XML probably comes from another site.
  7. If the preview looks right, run the same command without dry-run.
wp eval-file update-content-from-xml.php YOUR-FILE.xml dry-run
wp eval-file update-content-from-xml.php YOUR-FILE.xml

The script matches posts by ID. It does not create or delete posts, and it checks that the slug and date stay the same after each update. Only posts are processed. Pages and products are skipped.

Step 3: check and clean up

  1. Open a few posts and check the content and the links.
  2. Clear your site cache.
  3. Delete the script and the XML file from the server.
  4. Turn SSH off if you no longer need it.

Mistakes to avoid

  • Skipping the backup or the dry run.
  • Using an XML file from a different site, so the IDs do not match.
  • Leaving the files on the server after the job.

Downloads and prompts

Copy the text below, or download the files above.

Script: update-content-from-xml.php

<?php
/**
 * update-content-from-xml.php
 *
 * Updates the post_content of EXISTING posts on this site from a WordPress
 * WXR export file (.xml), matched by post ID. Nothing is created, nothing is
 * deleted. Only post_content of posts that already exist is changed. Slug,
 * title, date, categories and everything else are left alone.
 *
 * WHERE TO PUT IT:
 *   Upload this file AND your XML file into the WordPress root folder, the
 *   folder that contains wp-config.php (on Hostinger this is usually
 *   domains/YOURDOMAIN.com/public_html). Run the commands from that same folder.
 *
 * USAGE (WP-CLI over SSH, from the WordPress root):
 *
 *   # 1. Preview first. Shows what WOULD change, saves nothing:
 *   wp eval-file update-content-from-xml.php YOUR-FILE.xml dry-run
 *
 *   # 2. If the preview looks right, run it for real (drop "dry-run"):
 *   wp eval-file update-content-from-xml.php YOUR-FILE.xml
 *
 *   # Optional third argument: an administrator user ID to run as, so content
 *   # is not stripped by KSES filtering. Defaults to user ID 1.
 *   wp eval-file update-content-from-xml.php YOUR-FILE.xml dry-run 2
 *
 * NOTE: only items with post type "post" are processed. Pages, products and
 * other types are skipped (see the post type check below).
 */
$xml_path = isset( $args[0] ) ? $args[0] : null;
$dry_run  = isset( $args[1] ) && 'dry-run' === $args[1];
$user_id  = isset( $args[2] ) ? (int) $args[2] : 1;
if ( ! $xml_path || ! file_exists( $xml_path ) ) {
	WP_CLI::error( "Please provide a valid path to the XML file.nUsage: wp eval-file update-content-from-xml.php /path/to/file.xml [dry-run] [user-id]" );
}
// Run as an admin user so post_content isn't stripped by KSES filtering.
wp_set_current_user( $user_id );
$dom = new DOMDocument();
if ( ! $dom->load( $xml_path ) ) {
	WP_CLI::error( "Could not parse XML file: {$xml_path}" );
}
$xpath = new DOMXPath( $dom );
$xpath->registerNamespace( 'wp', 'http://wordpress.org/export/1.2/' );
$xpath->registerNamespace( 'content', 'http://purl.org/rss/1.0/modules/content/' );
$items = $xpath->query( '//item' );
$updated             = 0;
$unchanged           = 0;
$skipped_missing     = 0;
$skipped_wrong_type  = 0;
$errors              = 0;
foreach ( $items as $item ) {
	$post_type_node = $xpath->query( 'wp:post_type', $item )->item( 0 );
	$post_type      = $post_type_node ? $post_type_node->nodeValue : '';
	if ( 'post' !== $post_type ) {
		$skipped_wrong_type++;
		continue;
	}
	$post_id_node = $xpath->query( 'wp:post_id', $item )->item( 0 );
	$post_id      = $post_id_node ? (int) $post_id_node->nodeValue : 0;
	$title_node = $xpath->query( 'title', $item )->item( 0 );
	$title      = $title_node ? $title_node->nodeValue : '(untitled)';
	$content_node = $xpath->query( 'content:encoded', $item )->item( 0 );
	$new_content  = null !== $content_node ? $content_node->nodeValue : null;
	if ( ! $post_id || null === $new_content ) {
		echo "SKIP (missing post_id or content): {$title}n";
		$skipped_missing++;
		continue;
	}
	$existing = get_post( $post_id );
	if ( ! $existing ) {
		echo "SKIP (post ID {$post_id} not found on this site): {$title}n";
		$skipped_missing++;
		continue;
	}
	if ( $existing->post_content === $new_content ) {
		echo "UNCHANGED: [{$post_id}] {$title}n";
		$unchanged++;
		continue;
	}
	if ( $dry_run ) {
		$old_len = strlen( $existing->post_content );
		$new_len = strlen( $new_content );
		echo "WOULD UPDATE: [{$post_id}] {$title} ({$old_len} -> {$new_len} chars)n";
		$updated++;
		continue;
	}
	// Capture the slug and date BEFORE updating, so we can prove afterward
	// that this script did not touch either -- only post_content changes.
	$slug_before = $existing->post_name;
	$date_before = $existing->post_date;
	$result = wp_update_post(
		array(
			'ID'           => $post_id,
			'post_content' => $new_content,
		),
		true
	);
	if ( is_wp_error( $result ) ) {
		echo "ERROR: [{$post_id}] {$title} -- " . $result->get_error_message() . "n";
		$errors++;
	} else {
		// Safety check: confirm slug and date are unchanged after the update.
		$after = get_post( $post_id );
		if ( $after->post_name !== $slug_before || $after->post_date !== $date_before ) {
			echo "WARNING: [{$post_id}] {$title} -- slug or date changed unexpectedly! slug: '{$slug_before}' -> '{$after->post_name}', date: '{$date_before}' -> '{$after->post_date}'n";
		} else {
			echo "UPDATED: [{$post_id}] {$title} (slug and date confirmed unchanged: /{$slug_before}/, {$date_before})n";
		}
		$updated++;
	}
}
echo "n----------------------------------------n";
echo ( $dry_run ? 'DRY RUN ' : '' ) . "SUMMARYn";
echo "Updated:                {$updated}n";
echo "Unchanged (already set): {$unchanged}n";
echo "Skipped (not found):    {$skipped_missing}n";
echo "Non-post items skipped: {$skipped_wrong_type}n";
echo "Errors:                 {$errors}n";

Instructions for using the script

HOW TO UPDATE POST CONTENT FROM AN XML FILE (WP-CLI over SSH)
==============================================================
WHAT THIS DOES
- The script reads a WordPress XML export file (WXR) and updates the content
  of posts that ALREADY exist on your site, matching them by post ID.
- It does NOT create posts, delete posts, or change slugs, titles, dates or
  categories. After each update it checks that the slug and date are unchanged.
- Only items with type "post" are processed. Pages and products are skipped.
- The XML must come from the SAME website (post IDs must match).
BEFORE YOU START
- Take a full backup (hosting backup or a backup plugin). WordPress also keeps
  revisions of posts, but a backup is the safe option.
- You need: the script (update-content-from-xml.php), your XML file, and a
  hosting plan with SSH access and WP-CLI (Hostinger has both).
STEP 1: UPLOAD THE FILES
- Upload BOTH files (the .php script and your .xml file) into your WordPress
  root folder, the folder that contains wp-config.php.
- On Hostinger this is: domains/YOURDOMAIN.com/public_html
  (hPanel > Files > File Manager, or any FTP client such as FileZilla).
- Both files must be in the same folder.
STEP 2: ENABLE SSH
- Log in to your hosting dashboard (hPanel).
- Go to Advanced > SSH Access and turn SSH on.
- Set an SSH password there. The page shows your connection command, which looks like:
    ssh -p PORT USERNAME@SERVER-IP
STEP 3: CONNECT
- Open a terminal (Windows: PowerShell or Windows Terminal. Mac: Terminal).
- Paste the SSH command from the dashboard and press Enter.
- Type your SSH password and press Enter. Nothing shows while you type, that is normal.
STEP 4: GO TO YOUR WORDPRESS FOLDER
- Run (replace YOURDOMAIN.com with your domain):
    cd domains/YOURDOMAIN.com/public_html
- Check you are in the right place:
    ls
  You should see wp-config.php, your .php script and your .xml file in the list.
STEP 5: PREVIEW (DRY RUN, SAVES NOTHING)
- Run (replace YOUR-FILE.xml with your XML file name):
    wp eval-file update-content-from-xml.php YOUR-FILE.xml dry-run
- Good result: lines starting with WOULD UPDATE, and a summary at the end.
- Read the summary. If many posts say SKIP (not found), the XML is probably
  from a different site or the post IDs do not match. Stop and check.
STEP 6: RUN IT FOR REAL
- If the preview looks right, run the same command without "dry-run":
    wp eval-file update-content-from-xml.php YOUR-FILE.xml
- Each post prints UPDATED and confirms the slug and date did not change.
- If you see a WARNING line about slug or date, restore that post from backup/revision.
STEP 7: CHECK AND CLEAN UP
- Open a few posts on your site and check the content and links.
- Clear your site cache (cache plugin / hosting cache).
- Delete the .php script and the .xml file from public_html when finished,
  so they are not left publicly accessible.
- Turn SSH off again in hPanel if you no longer need it.
OPTIONAL
- Run as a specific administrator (default is user ID 1):
    wp eval-file update-content-from-xml.php YOUR-FILE.xml dry-run 2
TROUBLESHOOTING
- "wp: command not found": your host does not have WP-CLI. Ask support to enable it.
- "This does not seem to be a WordPress installation": you are in the wrong
  folder. Use cd to reach the folder that contains wp-config.php.
- "Please provide a valid path to the XML file": the file name is misspelled or
  the file is not in the folder you are in. Run ls to check.
- Multiple WordPress sites on one hosting account: make sure the cd path is the
  domain you want to change.

Prompt: add internal links to the XML file

==============================================================
PART 1: WHERE TO PLACE THE INTERNAL LINK URLs IN THE XML FILE
==============================================================
Use these rules when you (or an AI) add links to the XML before running the
script. They come from the Master SEO + Website Development System. The script
only copies the content into WordPress, so the placement has to be right in the
XML first.
WHAT TO CHANGE IN THE XML
- Edit ONLY the text inside <content:encoded> (keep it inside its CDATA block).
- Do not change post IDs, titles, slugs, dates, categories or any other field.
- Keep all existing text, images, shortcodes, block comments and old links.
THE 2-LINK PLACEMENT PATTERN (default for every article)
- Link 1 = an article / blog link. Put it in paragraph 1, 2 or 3 of the body.
  The paragraph must actually talk about the subject of the link.
- Link 2 = a product link or a page link. Put it in the paragraph immediately
  ABOVE the FAQ section (the last paragraph before the FAQ heading).
- Never put a link in the conclusion.
- When you do articles in pairs or batches, alternate Link 2: one article gets
  a product link above its FAQs, the next gets a page link above its FAQs.
- Both articles always get the article link near the top.
- For long guides you may add more links, but only where they really help the
  reader. If you ask for a fixed number, follow that number.
ANCHOR TEXT
- Use a natural phrase that already fits the sentence and matches the topic of
  the destination page. A real reader should want to click it.
- Never use "click here", "this page", "read more" or a raw URL.
- Read the destination URL first to understand its topic, then choose the anchor.
- Do not put links in headings (H1 to H6).
TOPIC MATCH AND LIMITS
- A link about topic A can only sit in a paragraph that talks about topic A.
  Never force it into a paragraph about topic B.
- One link per paragraph. Do not put two links in the same paragraph.
- Never link a post to itself. Do not link the same URL twice in one post.
WHICH URLs TO USE
- Use only approved URLs: posts from the XML, plus your reference list of
  pages and products (sitemap or a list you provide). Never invent a URL.
- Write internal links as root-relative paths, for example /your-post-slug/
  (not the full https://yourdomain.com/... address), so they work on any domain.
- Add external links only to cite a real statistic or an authoritative source,
  maximum 2 to 4 per article, with target="_blank". Do not use them to pad links.
HOW TO MAKE SURE NO POST IS ORPHANED
- Every post must receive at least 1 incoming link from another post.
- Easy way with the 2-link pattern: make Link 1 a chain. Group posts by topic
  and link them in a loop (A to B, B to C, C back to A). Then every post gets
  one incoming article link and gives one outgoing article link.
- Link 2 (product or page) can then be spread to your most important
  money pages, so those pages also get internal links from the blog.
- Ask the AI for a report table (post, outgoing links, incoming links) and
  check that no post shows 0 incoming links before you upload the XML.
FINAL CHECK BEFORE UPLOADING THE XML
- Every article has Link 1 in paragraphs 1 to 3, and Link 2 right above the FAQs.
- No link inside the conclusion or in any heading.
- Anchors are natural and not repeated for the same target everywhere.
- The XML still opens as valid XML and the number of posts is the same as before.
==============================================================
PART 2: PROMPT TO GIVE THE AI (attach your XML + approved URL list)
==============================================================
I am attaching a WordPress export file (WXR .xml) of my posts, plus a list of
approved URLs (articles, pages, products). Add internal links so that NO post is
orphaned, then return the full updated XML.
STEP 1: LINK PLAN (do this before writing anything)
- Every post gets exactly 2 internal links:
  Link 1 = an article/blog link. Link 2 = a product or page link.
- Orphan prevention: group the posts by topic and chain Link 1 in a loop inside
  each group (A to B, B to C, last back to A) so every post receives at least
  1 incoming article link. Connect small groups to each other if needed.
- Spread Link 2 across my important product and page URLs. Alternate Link 2
  between a product link and a page link from one article to the next.
- Show me the plan as a table first: Post title | Post ID | Link 1 target |
  Link 2 target | Incoming links.
STEP 2: PLACEMENT RULES
- Link 1: in paragraph 1, 2 or 3 of the body, in a paragraph that actively
  discusses the target's subject.
- Link 2: in the paragraph immediately above the FAQ section.
- Never put a link in the conclusion, in any heading, or in an image caption.
- One link per paragraph. No post links to itself. No repeated URL in one post.
- A link may only sit in a paragraph that really talks about that topic.
- If a post has fewer than 3 paragraphs or no FAQ section, place the link in the
  closest suitable paragraph and tell me which posts were affected.
STEP 3: ANCHOR TEXT AND URLS
- Anchor = a natural phrase already in the sentence, related to the target
  page's topic. Read the target URL to understand its topic. 2 to 5 words.
- Never use "click here", "this page", "read more" or a raw URL. Vary anchors.
- Use only URLs from the XML <link> fields or my approved list. Never invent URLs.
- Write links as root-relative paths without the domain, like
  <a href="/post-slug/">anchor text</a>.
- Do not add external links.
STEP 4: XML SAFETY (very important)
- Change only the text inside <content:encoded>, keep it inside its CDATA block.
- Keep every wp:post_id, title, link, wp:post_name, date, category and all other
  fields exactly the same. Do not add, remove or reorder items.
- Keep all existing text, HTML, images, shortcodes, block comments and old links
  exactly as they are. Do not rewrite or shorten the content.
- The output must be valid XML.
- If the file is too big for one reply, work in batches of 10 posts and keep the
  same plan across batches.
OUTPUT
- The link plan table, then confirm: every post has 2 links, Link 1 is in
  paragraphs 1 to 3, Link 2 is right above the FAQs, nothing is in the
  conclusion, and no post has 0 incoming links.
- Then the complete updated XML file.

Prompt: write bulk articles as an XML file

FOR GENERATING XML FILE FROM INFORMATION OR ANY EXISTING POST FILE
YOU HAVE TO GIVE XML FILE EXPORTED FROM ANY WEBSITE THAT HAS ALL META DATA AND IMAGES 
YOU HAVE TO GIVE THE TOPICS 
YOU  HAVE TO GIVE THE KEYWORDS 
YOU HAVE TO GIVE THE ENTITIES 
YOU HAVE TO GIVE THE CONTENT WRITING INSTRUCTIONS FILE
Hey i am giving you the xml file in which all the wordpress post structure is present you have to use this file as a reference and structure and give me the following outputs 
Give me the content on all the topics that i share you 
I have give you the keywords and entities and you have to analyze and add them into the posts 
The content writing instructions and internal linking instructions are in the master content writing instruction file 
Now you have to generate the xml file of all the posts to import in any website 
Search the images related to the article and place the link of the image in xml file 
Then you have to make sure that add the internal links in this system you have to embed the internal link on the relevant keyword or anchor text make sure add the one link at the beginning of the article and one before the faq 
Don't copy the content from this provided xml file because it has only idea for you to generate the xml file 
Generate the content almost 2500 words 
Pickup the internal linking and content writing and keyword adjustment all the seo instructions from the file 
Add the categories, schema, meta title and description, focus keyword, tags (upto 5), canonical url, etc according to the content or article 
Internal link all the post by generating slug and avoid making orphaned posts pass the link to every post 

Tools used

WordPress export, an AI assistant, SSH, WP-CLI, a small update script.

Need this done for you? See my technical SEO service.

← All solutions

Keep reading

Related guides

Start typing to see posts you are looking for.