Posts Tagged ‘Wordpress’
Enabling and Using the WordPress REST API on OVH Hosting
I recently started migrating my WordPress site from Free.fr to OVHcloud hosting. The migration is still in progress, but along the way I needed to enable and validate
programmatic publishing through the WordPress REST API (RPC/API calls). This post documents the full process end-to-end, including OVH-specific gotchas and troubleshooting.
My last migration was many years ago, from DotClear 2 to WordPress…
Why move from Free.fr to OVH?
- Performance: More CPU/RAM and faster PHP execution make WordPress snappier.
- Modern PHP: Current PHP versions and extensions are available and easy to select.
- HTTPS (SSL): Essential for secure logins and required for Application Passwords.
- Better control: You can tweak
.htaccess
, install custom/mu-plugins, and adjust config. - Scalability: Easier to upgrade plans and resources as your site grows.
What is the WordPress REST API?
WordPress ships with a built-in REST API at /wp-json/
. It lets you read and write content, upload media, and automate publishing from scripts or external systems (curl, Python, Node.js, CI, etc.).
Step 1 — Confirm the API is reachable
- Open
https://yourdomain.com/wp-json/
in a browser. You should see a JSON index of routes. - Optional: check
https://yourdomain.com/wp-json/wp/v2
or
https://yourdomain.com/wp-json/wp/v2/types/post
to view available endpoints and fields.
Step 2 — Enable authentication with Application Passwords
- Sign in to
/wp-admin/
with a user that can create/publish posts. - Go to Users → Profile (your profile page).
- In Application Passwords, add a new password (e.g., “API access from laptop”). It should look like
ABCD EFgh IjKl M123 n951
(including spaces)
- Copy the generated password (you’ll only see it once). Keep it secure.
You will authenticate via HTTP Basic Auth using username:application-password
over HTTPS.
Step 3 — Test authentication (curl)
Replace the placeholders before running:
curl -i -u 'USERNAME:APP_PASSWORD' \
https://yourdomain.com/wp-json/wp/v2/users/me
Expected result: 200 OK
with your user JSON. If you get 401
or 403
, see Troubleshooting below.
Important on OVH — The Authorization
header may be stripped
On some OVH hosting configurations, the HTTP Authorization
header isn’t passed to PHP.
If that happens, WordPress cannot see your Application Password and responds with:
{"code":"rest_not_logged_in","message":"You are not currently logged in.","data":{"status":401}}
To confirm you’re sending the header, try explicitly setting it:
curl -i -H "Authorization: Basic $(echo -n 'USERNAME:APP_PASSWORD' | base64)" \
https://yourdomain.com/wp-json/wp/v2/users/me
If you still get 401
, fix the server so PHP receives the header.
Step 4 — Fixing Authorization
headers on OVH
Option A — Add rules to .htaccess
Connect in FTP, browse to “www” folder, edit the .htaccess file. Add these lines above the “BEGIN WordPress” block:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
</IfModule>
<IfModule mod_setenvif.c>
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
</IfModule>
Option B — Tiny must-use plugin
Create wp-content/mu-plugins/
if missing, then add fix-authorization.php
:
<?php
/**
* Plugin Name: Fix Authorization Header
* Description: Ensures HTTP Authorization header is passed to WordPress for Application Passwords.
*/
add_action('init', function () {
if (!isset($_SERVER['HTTP_AUTHORIZATION'])) {
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
} elseif (function_exists('apache_request_headers')) {
$headers = apache_request_headers();
if (isset($headers['Authorization'])) {
$_SERVER['HTTP_AUTHORIZATION'] = $headers['Authorization'];
}
}
}
});
Upload and reload: authentication should now succeed.
Step 5 — Create and publish a complete post via API
Optional: create a category and a tag
# Create a category
curl -i -X POST \
-u 'USERNAME:APP_PASSWORD' \
-H "Content-Type: application/json" \
-d '{ "name": "Tech" }' \
https://yourdomain.com/wp-json/wp/v2/categories
# Create a tag
curl -i -X POST \
-u 'USERNAME:APP_PASSWORD' \
-H "Content-Type: application/json" \
-d '{ "name": "API" }' \
https://yourdomain.com/wp-json/wp/v2/tags
Upload a featured image
curl -i -X POST \
-u 'USERNAME:APP_PASSWORD' \
-H "Content-Disposition: attachment; filename=header.jpg" \
-H "Content-Type: image/jpeg" \
--data-binary @/full/path/to/header.jpg \
https://yourdomain.com/wp-json/wp/v2/media
Note the returned MEDIA_ID
.
Create and publish the post
curl -i -X POST \
-u 'USERNAME:APP_PASSWORD' \
-H "Content-Type: application/json" \
-d '{
"title": "Hello from the API",
"content": "<p>Created automatically 🚀</p>",
"status": "publish",
"categories": [CAT_ID],
"tags": [TAG_ID],
"featured_media": MEDIA_ID
}' \
https://yourdomain.com/wp-json/wp/v2/posts
Optionally update excerpt or slug
POST_ID=REPLACE_WITH_ID
curl -i -X POST \
-u 'USERNAME:APP_PASSWORD' \
-H "Content-Type: application/json" \
-d '{ "excerpt": "Short summary", "slug": "hello-from-the-api" }' \
https://yourdomain.com/wp-json/wp/v2/posts/$POST_ID
Troubleshooting
- 401 Unauthorized /
rest_not_logged_in
TheAuthorization
header isn’t reaching PHP. Add the.htaccess
rules or the mu-plugin above. Re-test with
-H "Authorization: Basic …"
. - 403 ForbiddenThe user lacks capabilities (e.g., Authors can’t publish globally). Use
"status":"draft"
or run publishing as an Editor/Admin. - Media upload failsCheck
upload_max_filesize
,post_max_size
, and file permissions. Try a smaller file to isolate the issue. - Categories/Tags not appliedUse numeric IDs, not names. Fetch with
/wp-json/wp/v2/categories
and/wp-json/wp/v2/tags
. - PermalinksPrefer non-Plain permalinks. If using Plain, you can call endpoints with the fallback:
https://yourdomain.com/?rest_route=/wp/v2/posts
.
Conclusion
Moving from Free.fr to OVH brings better performance, modern PHP, and full HTTPS, which is perfect for automation and scheduling.
After ensuring the Authorization
header reaches WordPress (via .htaccess
or a tiny mu-plugin), the REST API works smoothly for creating posts, uploading media, and managing taxonomy.
My migration is still ongoing, but having a reliable API in place is already a big win.
Quick and dirty script to convert WordPress export file to Blogger / Atom XML
I’ve created a Python script that converts WordPress export files to Blogger/Atom XML format. Here’s how to use it:
The script takes two command-line arguments:
wordpress_export.xml
: Path to your WordPress export XML fileblogger_export.xml
: Path where you want to save the converted Blogger/Atom XML file
To run the script:
python wordpress_to_blogger.py wordpress_export.xml blogger_export.xml
The script performs the following conversions:
- Converts WordPress posts to Atom feed entries
- Preserves post titles, content, publication dates, and authors
- Maintains categories as Atom categories
- Handles post status (published/draft)
- Preserves HTML content formatting
- Converts dates to ISO format required by Atom
The script uses Python’s built-in xml.etree.ElementTree
module for XML processing and includes error handling to make it robust.
Some important notes:
- The script only converts posts (not pages or other content types)
- It preserves the HTML content of your posts
- It maintains the original publication dates
- It handles both published and draft posts
- The output is a valid Atom XML feed that Blogger can import
The file:
#!/usr/bin/env python3 import xml.etree.ElementTree as ET import sys import argparse from datetime import datetime import re def convert_wordpress_to_blogger(wordpress_file, output_file): # Parse WordPress XML tree = ET.parse(wordpress_file) root = tree.getroot() # Create Atom feed atom = ET.Element('feed', { 'xmlns': 'http://www.w3.org/2005/Atom', 'xmlns:app': 'http://www.w3.org/2007/app', 'xmlns:thr': 'http://purl.org/syndication/thread/1.0' }) # Add feed metadata title = ET.SubElement(atom, 'title') title.text = 'Blog Posts' updated = ET.SubElement(atom, 'updated') updated.text = datetime.now().isoformat() # Process each post for item in root.findall('.//item'): if item.find('wp:post_type', {'wp': 'http://wordpress.org/export/1.2/'}).text != 'post': continue entry = ET.SubElement(atom, 'entry') # Title title = ET.SubElement(entry, 'title') title.text = item.find('title').text # Content content = ET.SubElement(entry, 'content', {'type': 'html'}) content.text = item.find('content:encoded', {'content': 'http://purl.org/rss/1.0/modules/content/'}).text # Publication date pub_date = item.find('pubDate').text published = ET.SubElement(entry, 'published') published.text = datetime.strptime(pub_date, '%a, %d %b %Y %H:%M:%S %z').isoformat() # Author author = ET.SubElement(entry, 'author') name = ET.SubElement(author, 'name') name.text = item.find('dc:creator', {'dc': 'http://purl.org/dc/elements/1.1/'}).text # Categories for category in item.findall('category'): category_elem = ET.SubElement(entry, 'category', {'term': category.text}) # Status status = item.find('wp:status', {'wp': 'http://wordpress.org/export/1.2/'}).text if status == 'publish': app_control = ET.SubElement(entry, 'app:control', {'xmlns:app': 'http://www.w3.org/2007/app'}) app_draft = ET.SubElement(app_control, 'app:draft') app_draft.text = 'no' else: app_control = ET.SubElement(entry, 'app:control', {'xmlns:app': 'http://www.w3.org/2007/app'}) app_draft = ET.SubElement(app_control, 'app:draft') app_draft.text = 'yes' # Write the output file tree = ET.ElementTree(atom) tree.write(output_file, encoding='utf-8', xml_declaration=True) def main(): parser = argparse.ArgumentParser(description='Convert WordPress export to Blogger/Atom XML format') parser.add_argument('wordpress_file', help='Path to WordPress export XML file') parser.add_argument('output_file', help='Path to output Blogger/Atom XML file') args = parser.parse_args() try: convert_wordpress_to_blogger(args.wordpress_file, args.output_file) print(f"Successfully converted {args.wordpress_file} to {args.output_file}") except Exception as e: print(f"Error: {str(e)}") sys.exit(1) if __name__ == '__main__': main()
[PHPForumParis 2024] Is WordPress a lost cause?
Cyrille Coquard, a seasoned WordPress developer, took the stage at PHPForumParis2024 to address a contentious question: Is WordPress a lost cause? With humor and insight, Cyrille tackles the platform’s reputation, often marred by perceptions of outdated code and technical debt. By drawing parallels between WordPress and PHP’s evolution, he argues that WordPress is undergoing a quiet transformation toward professionalism. His talk, aimed at PHP developers, demystifies WordPress’s architecture and advocates for modern development practices to elevate its potential.
The Shared Legacy of WordPress and PHP
Cyrille opens by highlighting the intertwined histories of WordPress and PHP, both born in an era of amateur-driven development. This shared origin, while fostering accessibility, has led to technical debt that tarnishes their reputations. He compares WordPress to a “Fiat or Clio”—a practical, accessible tool for the masses—contrasting it with frameworks like Symfony, likened to a high-end race car. This metaphor underscores WordPress’s mission to democratize web creation, prioritizing user-friendliness over developer-centric complexity. Cyrille emphasizes that the platform’s early design choices, while not always optimal, reflect its commitment to simplicity and affordability.
Plugins and Themes: Extending WordPress’s Power
A core strength of WordPress lies in its extensibility through plugins and themes. Cyrille explains how themes allow for visual customization, enabling the 40% of websites powered by WordPress to look unique. Plugins, meanwhile, add functionality or modify behavior, addressing both generic and specific user needs. He illustrates this with examples like WooCommerce for e-commerce and Gravity Forms for form creation. By leveraging pre-existing plugins, developers can meet common requirements efficiently, reserving custom development for unique challenges. This approach, Cyrille notes, significantly reduces costs, as seen in his work at WP Rocket, where a single plugin optimizes performance across millions of sites.
Modernizing WordPress Development with Launchpad
To address WordPress’s development challenges, Cyrille introduces Launchpad, his open-source framework designed to bring modern PHP practices to the WordPress ecosystem. Inspired by Laravel and Symfony, Launchpad simplifies plugin creation by introducing concepts like subscribers and service providers. These patterns, familiar to PHP developers, reduce the learning curve for newcomers while promoting clean, maintainable code. Cyrille demonstrates how to create a simple plugin, emphasizing event-driven development that hooks into WordPress’s core functionality. By providing a standardized, well-documented framework, Launchpad aims to bridge the gap between WordPress’s amateur roots and professional standards.
Links:
Hashtags: #WordPress #PHP #WebDevelopment #Launchpad #PHPForumParis2024 #CyrilleCoquard #WPRocket
Blog Upgrade onto WordPress 3.3.1 on Free.fr
Yesterday I upgraded the blog to WordPress 3.3.1. Last version was a but old (2.8 branch), I installed it in october 2009.
Being hosted on Free.fr, I had to use a customized version of WordPress, released by Gaetan Janssens on his blog Petit Nuage’s Stunning World.
The process I followed is basic:
- back up database via PhpMyAdmin
- export the blog full content
- backup current state of (former) remote WordPress code
- upload WordPress 3.3 via FTP
- reupload once more (I often happened to have files that Free.fr FTP “missed” to receive, or received partially ; I don’t think FileZilla is the root cause)
- add a
.htaccess
(the former one vanished in outer space, I ignore why) - login to admin
- disable all plugins
- restore default them
- display the blog
- enable theme
- enable each plugin one per one
I encountered some issues, that I fixed after a short look in PHP code. Well… I was a PHP expert ; I am no more :-D. I may speak Spanish better than PHP.
Now it seems to work. So far, having kept the same theme, almost no differences are visible. I only added links and social sharing sections on the left column. Anyway I’d like to change the theme (even though I enjoy it and its Tux 😉 , and I’d like to keep a Linux-oriented style)
Akismet does not work anymore (more information on Pascal Ledisque’s bloc, in French). I may use Antispam Bee instead.
I also was unable to display Twitter flow: this issue is linked to the previous one: Free.fr prevents WordPress from accessing external HTML, XML and/or RSS flows.