Fill Yoast Meta Data and Schema in Bulk With a PHP Script and SSH
Bulk update Yoast SEO metadata and custom schema across hundreds of WordPress posts using CSV, JSON, SSH, and WP-CLI. Follow this step-by-step guide.
Read the guide →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.

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.
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.
cd and check that wp-config.php is there with ls.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.
Copy the text below, or download the files above.
<?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";
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.
==============================================================
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.
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
WordPress export, an AI assistant, SSH, WP-CLI, a small update script.
Need this done for you? See my technical SEO service.
Bulk update Yoast SEO metadata and custom schema across hundreds of WordPress posts using CSV, JSON, SSH, and WP-CLI. Follow this step-by-step guide.
Read the guide →A poor WooCommerce CSV can break prices, images or variations. Follow a careful workflow to create, update and check imports. See the steps.
Read the guide →