Sharing is caring… Especially with code

How to Display Your Latest WordPress Posts on a PHP Homepage

Written by

Introduction

Have you ever looked at your own website and realized it hasn’t changed in a while? That’s where I found myself recently. I have a personal website that I hadn’t done much with for quite some time, and I wanted to give visitors a reason to come back.

I also wanted an easy way to show my latest blog articles on the homepage without having to edit the homepage every time I published something new.

The solution was pretty simple: use the RSS feed that WordPress already provides, pull in the latest three posts, and display them on my PHP homepage.

WordPress does most of the heavy lifting for us. We just need a little PHP to retrieve the feed, process the information, and display it. We’ll also add caching so the homepage doesn’t have to contact WordPress every time somebody visits.

In this tutorial, I’ll walk through the approach I used, explain how the code works, and cover a few things that can go wrong along the way.

You don’t need to build a plugin or install anything in WordPress. If you already have a PHP page on your website, you can add this functionality directly to it.

What We’re Building

The goal is to display the three most recent articles from a WordPress blog on a separate PHP homepage.

Each article will show:

  • The article title, linked to the full post.
  • The publication date.
  • A short excerpt from the article.

When you publish a new post in WordPress, the homepage can pick it up automatically the next time the feed is refreshed. There’s no need to manually update the homepage every time you write something new.

We’ll also account for a few practical issues. The feed might be temporarily unavailable, the cache file might be missing or corrupted, and a post might contain punctuation or characters that need special handling. We’ll build in reasonable fallbacks without making the solution unnecessarily complicated.

One thing worth mentioning up front: this is a way to keep your homepage current and connect visitors to your latest content. It isn’t a guarantee of better search rankings.

Before You Begin

You’ll need:

  • A WordPress site with an RSS feed enabled.
  • A PHP homepage where you can add the code.
  • PHP with the SimpleXML extension enabled.
  • PHP’s allow_url_fopen setting enabled so file_get_contents() can retrieve a remote feed.
  • A directory where PHP can create and update a cache file.

A standard WordPress RSS feed is usually available at:

https://yourdomain.com/blog/feed/

Replace that example with the actual address of your WordPress installation. If WordPress is installed at the root of your domain rather than in a /blog/ directory, the feed URL will be different.

You can test the address by opening it in a browser. You should see XML containing your latest posts.

The PHP Code

The following code is designed to be added to an existing PHP page. It fetches the latest posts, caches the results, validates links, and escapes text before displaying it.

Before using it, change the feed URL near the top to match your own WordPress site.

<?php

// Config Settings
$feed_url       = 'https://yourdomain.com/blog/feed/'; // Edit to your Blog URL
$cache_file     = __DIR__ . '/blog_feed_cache.json';
$cache_time     = 3600; // Cache duration in seconds (3600 = 1 hour)
$article_count  = 3;
$excerpt_length = 150;

$posts = [];
$load_from_feed = true;

// Helper function for multibyte-safe text truncation
function safe_truncate($text, $length = 150)
{
    // Check if the mbstring extension is enabled on the server
    if (function_exists('mb_strlen') && function_exists('mb_substr')) {
        if (mb_strlen($text, 'UTF-8') > $length) {
            return mb_substr($text, 0, $length, 'UTF-8') . '...';
        }
    } else {
        // Fallback using a Unicode-aware regular expression if mbstring is missing
        if (strlen($text) > $length) {
            $truncated = substr($text, 0, $length);
            // Remove trailing bytes until the text is valid UTF-8
            // This handles a character cut in half at the end
            while ($truncated !== '' && preg_match('//u', $truncated) !== 1) {
                $truncated = substr($truncated, 0, -1);
            }
            return $truncated . '...';
        }
    }
    return $text;
}

// Cache Check: Ensure it exists, is fresh, and decodes to a valid array
if (file_exists($cache_file) && (time() - filemtime($cache_file) < $cache_time)) {
    $raw_cache = @file_get_contents($cache_file);
    if ($raw_cache !== false) {
        $cache_data = json_decode($raw_cache, true);
        if (is_array($cache_data) && !empty($cache_data)) {
            $posts = $cache_data;
            $load_from_feed = false; // Bypass network request
        }
    }
}

// Fetch from feed if cache is expired or missing
if ($load_from_feed) {
    $context = stream_context_create(['http' => ['timeout' => 3]]);
    $xml_string = @file_get_contents($feed_url, false, $context);

    if ($xml_string !== false) {
        $rss = @simplexml_load_string($xml_string, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NONET);

        if ($rss && isset($rss->channel->item)) {
            $items = $rss->channel->item;
            $count = 0;
            $fresh_posts = [];

            foreach ($items as $item) {
                if ($count >= $article_count) break;

                // Process description / content fallback
                $description = '';
                if (!empty($item->description)) {
                    $description = (string)$item->description;
                } elseif (isset($item->children('content', true)->encoded)) {
                    $description = (string)$item->children('content', true)->encoded;
                }

                $description = strip_tags($description);
                $description = trim(preg_replace('/\s+/', ' ', $description));

                // Decode HTML entities so they display as normal characters
                $title_raw = html_entity_decode((string)$item->title, ENT_QUOTES | ENT_HTML5, 'UTF-8');
                $desc_raw  = html_entity_decode($description, ENT_QUOTES | ENT_HTML5, 'UTF-8');

                // OPTIONAL: Replace smart quotes and typographic dashes with plain punctuation
                // Comment out this block to preserve the original typography
                $search  = ['’', '‘', '”', '“', '–', '—'];
                $replace = ["'", "'", '"', '"', '-', '-'];

                $title_raw = str_replace($search, $replace, $title_raw);
                $desc_raw  = str_replace($search, $replace, $desc_raw);

                // Use safe helper function to truncate cleanly
                $desc_raw = safe_truncate($desc_raw, $excerpt_length);

                // Pack data into an array format for easy JSON serialization
                $fresh_posts[] = [
                    'title'       => $title_raw,
                    'link'        => (string)$item->link,
                    'pub_date'    => (string)$item->pubDate,
                    'description' => $desc_raw
                ];

                $count++;
            }

            // Verify json_encode succeeded before saving to file
            if (!empty($fresh_posts)) {
                $encoded_data = json_encode($fresh_posts, JSON_PRETTY_PRINT);
                if ($encoded_data !== false && json_last_error() === JSON_ERROR_NONE) {
                    $posts = $fresh_posts;
                    @file_put_contents($cache_file, $encoded_data);
                }
            }
        }
    }

    // If the live fetch failed but an old cache exists, use old data as a fallback safety net
    if (empty($posts) && file_exists($cache_file)) {
        $raw_cache = @file_get_contents($cache_file);
        if ($raw_cache !== false) {
            $cache_data = json_decode($raw_cache, true);
            if (is_array($cache_data)) {
                $posts = $cache_data;
            }
        }
    }
}

// Track how many posts were rendered
$render_count = 0;

// Output the HTML Markup (With Late Escaping & URL Validation)
if (!empty($posts) && is_array($posts)) {
    foreach ($posts as $post) {
        if (!is_array($post)) {
            continue; // Skip malformed non-array elements if cache got corrupted
        }

        // Defensive default values if fields are missing from cache entries
        $raw_url   = isset($post['link']) ? (string)$post['link'] : '';
        $raw_title = isset($post['title']) ? (string)$post['title'] : 'Untitled Post';
        $raw_desc  = isset($post['description']) ? (string)$post['description'] : '';
        $raw_date  = isset($post['pub_date']) ? (string)$post['pub_date'] : '';

        // Validate URL Scheme strictly for HTTP / HTTPS and its format
        $valid_url = filter_var($raw_url, FILTER_VALIDATE_URL);
        $scheme = strtolower((string) parse_url($raw_url, PHP_URL_SCHEME));
        if ($valid_url === false || !in_array($scheme, ['http', 'https'], true)) {
            continue;
        }

        // Context-aware escaping right at the moment of print
        $title       = htmlspecialchars($raw_title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        $link        = htmlspecialchars($raw_url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        $description = htmlspecialchars($raw_desc, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

        // Format the publish date safely
        $timestamp = strtotime($raw_date ?? '');
        $pub_date  = ($timestamp !== false) ? date('F j, Y', $timestamp) : '';

        echo '<div class="blog-feed-post">';
        echo '  <h3><a href="' . $link . '">' . $title . '</a></h3>';
        echo '  <span class="feed-date"><small>' . $pub_date . '</small></span>';
        echo '  <p class="feed-excerpt">' . $description . '</p>';
        echo '</div>';

        $render_count++;
    }
}

if (!$render_count) {
    // Ultimate fallback if no feed data, no cache exist yet, and no posts rendered
    echo '<div class="blog-feed-failover">';
    echo '  <p>Please check back later for new blog articles.</p>';
    echo '</div>';
}

?>

How It Works

Let’s break the code into smaller pieces. You don’t need to memorize every line. The important part is understanding what each section does and knowing which settings you can change.

1. Configure the feed and cache

At the top of the script, you’ll find the settings:

$feed_url       = 'https://yourdomain.com/blog/feed/';
$cache_file     = __DIR__ . '/blog_feed_cache.json';
$cache_time     = 3600;
$article_count  = 3;
$excerpt_length = 150;

These are the values you’ll most likely want to adjust.

$feed_url is the address of your WordPress RSS feed.

$cache_file is where the script stores the retrieved posts. Using __DIR__ means the cache file is placed in the same directory as the PHP script, rather than relying on whatever the current working directory happens to be.

$cache_time controls how long the cached data is considered fresh. The value is measured in seconds, so 3600 equals one hour.

$article_count sets how many posts the script retrieves, while $excerpt_length controls the maximum excerpt length in characters when truncation is needed.

Want to display five articles instead of three? Change $article_count to 5. Want shorter excerpts? Reduce $excerpt_length.

Simple enough.

2. Check the cache before contacting WordPress

There’s no reason to request the same RSS feed on every homepage visit if we already have a reasonably recent copy.

The script checks whether the JSON cache file exists and whether it is younger than the configured cache duration. If the file contains valid, nonempty JSON data, the script uses it and skips the network request.

This is the main benefit of caching. It reduces repeated requests to WordPress and allows the homepage to continue displaying saved posts even when the feed is temporarily unavailable.

The cache is not a permanent record, though. Once it expires, the script attempts to retrieve fresh data.

3. Retrieve and parse the RSS feed

When the cache is missing, expired, or unusable, the script attempts to download the feed:

$context = stream_context_create(['http' => ['timeout' => 3]]);
$xml_string = @file_get_contents($feed_url, false, $context);

The timeout limits how long the request waits for a response. A short timeout helps prevent a slow or unavailable feed from holding up the homepage for too long.

If the feed is retrieved successfully, simplexml_load_string() parses the XML so PHP can work with its elements.

This example expects the standard WordPress RSS 2.0 structure, where posts appear as items inside the feed’s channel.

The script then loops through those items and collects the fields needed for the homepage.

4. Clean up the title and excerpt

RSS descriptions can contain HTML markup, extra whitespace, and encoded characters. We don’t want to dump all of that directly onto the homepage.

The script removes HTML tags from the description, normalizes whitespace, and decodes HTML entities so encoded characters can be processed as normal text.

For example, an encoded apostrophe can be converted into a regular apostrophe. The optional punctuation block also replaces typographic quotation marks and dashes with plain equivalents. If you prefer to preserve the original typography, you can remove or comment out that block.

The safe_truncate() helper limits the excerpt length without intentionally cutting a UTF-8 character in half. It uses PHP’s mbstring functions when they’re available and has a fallback for servers without that extension.

One small detail: the script only adds the trailing ellipsis when the text actually exceeds the configured length.

5. Save the results as JSON

After collecting the posts, the script converts the data into JSON and writes it to the cache file.

JSON is a convenient format for this job because it can store the title, link, publication date, and description for each post without requiring another database table.

The script checks whether JSON encoding succeeded before attempting to save it. If the feed cannot be fetched or parsed successfully, the script can fall back to previously cached data.

Keep in mind that the PHP process needs permission to write to the directory containing the cache file. If it doesn’t, the script may still display freshly fetched posts, but it won’t be able to reuse those posts from the cache on the next request.

6. Validate and display the posts

Before displaying each post, the script checks that the cached entry is an array and supplies fallback values for missing fields.

It then validates the post URL and accepts only valid HTTP or HTTPS links. If a URL fails validation, that entry is skipped rather than being rendered as a clickable link.

The title, URL, and description are escaped with htmlspecialchars() immediately before they are printed into the HTML. This is important because data retrieved from another source should not automatically be treated as safe HTML.

Finally, the script formats the publication date and outputs each post inside a set of HTML elements.

The blog-feed-post, feed-date, feed-excerpt, and blog-feed-failover classes are there so you can style the output with your own CSS. They aren’t special PHP or WordPress classes.

The render counter tracks how many posts were actually displayed. If no entries survive the checks, the script displays a simple fallback message instead of leaving the section completely empty.

Adding It to Your Homepage

Once you’ve updated the feed URL, place the code inside your existing PHP page where you want the latest articles to appear.

The script outputs its own HTML, so you can wrap it in your existing page layout or Bootstrap containers. For example, your surrounding page might have a section heading such as "Latest Articles," followed by the generated post list.

The CSS is entirely up to you. You might give each article a little spacing, make the titles stand out, and use a smaller font for the publication date. The code deliberately leaves those design choices to your existing site.

If you’re adding the code to a page that already contains PHP, make sure you integrate it into the existing PHP structure correctly rather than inserting a second opening <?php tag inside an active PHP block.

Understanding the Cache

The cache duration is one of the most useful settings in the script.

With the default one-hour duration, a visitor may see the previous cached list until that cache expires. If you publish a new article, it won’t necessarily appear on the homepage immediately. Once the cache is considered expired, the next request that runs the script will attempt to refresh it.

You can adjust the duration to suit your publishing schedule. For example:

$cache_time = 3600;   // 1 hour
$cache_time = 86400;  // 1 day
$cache_time = 604800; // 1 week

Use only one of these assignments in your actual configuration.

If you publish frequently and want new posts to appear sooner, a shorter duration makes sense. If you publish once a week, a longer duration may be perfectly reasonable.

You can also delete the cache file to force the script to retrieve the feed again on the next request. Just remember that the PHP process must be able to recreate the file if you want caching to continue working.

Customize the HTML and CSS for Your Website

The HTML classes in this example, such as blog-feed-post, feed-date, feed-excerpt, and blog-feed-failover, are simply examples I used to organize the output. They’re not required names, and you can change them to whatever makes sense for your website.

You have two options. You can keep the existing HTML and write CSS to style those classes, or you can edit the PHP echo statements to generate the HTML structure your site already uses.

For example, if your website uses Bootstrap, you might want to replace the example markup with Bootstrap containers, rows, cards, or whatever fits your existing layout. You could also keep some of the original classes and add your own.

The important thing is that the PHP handles retrieving and processing the posts. How those posts look on the page is entirely up to you. Treat the HTML in this tutorial as a starting point and adapt it to your own site’s design.

Troubleshooting

Nothing appears on the homepage

First, open your RSS feed URL in a browser and verify that it returns the expected WordPress feed. Then check that the URL in the PHP configuration matches it.

If the feed itself works, check that PHP has the SimpleXML extension enabled and that allow_url_fopen permits remote URL access.

The homepage shows old articles

The script may still be using a fresh cache file. Wait for the configured cache period to expire or remove the cache file to trigger another fetch.

Also remember that the feed itself must contain the new article before the script can display it.

The feed works, but the cache never updates

Check the permissions on the directory containing blog_feed_cache.json. The PHP process needs write access to create or update the file.

Because the script suppresses warnings on certain operations, a failed write may not produce a visible error on the page. Check your server’s PHP or web-server logs if you need more information.

The feed is temporarily unavailable

If the feed request fails and an older cache file exists, the script attempts to use that saved data. This allows the homepage to keep showing previously retrieved posts instead of immediately losing the entire section.

If there is no usable cache and no posts can be rendered, the fallback message appears.

Apostrophes or other characters look wrong

The script decodes HTML entities when processing the title and description, then escapes the resulting text when it generates the HTML. The optional punctuation replacement also converts common smart quotes and typographic dashes to plain punctuation.

If you still see unexpected characters, inspect the actual feed content and verify that the page is being served with the expected UTF-8 character encoding.

A Few Final Notes

This example is intentionally focused on a standard WordPress RSS feed and a PHP homepage. It isn’t intended to be a universal RSS reader for every feed format.

The cache file stores public post information, not secrets, but you should still consider how files in your web directory can be accessed. If you prefer to keep the JSON file from being directly requested through a browser, store it outside the public web root and update $cache_file to point to that location, provided PHP has the required permissions.

And remember that caching introduces a small delay between publishing a post and seeing it on your homepage. That’s expected behavior, not necessarily a problem.

Conclusion

This was a small addition to an existing website, but it solved a practical problem for me. I can publish an article on WordPress, and my PHP homepage can pick up the latest posts without requiring me to edit the homepage manually.

The RSS feed provides the content, PHP handles the processing, and a simple JSON cache reduces unnecessary requests. The result is a homepage that can stay current with very little ongoing maintenance.

Sometimes the best solution isn’t another plugin or a complicated integration. It’s a small piece of code that does exactly what you need, fits into the system you already have, and gets out of your way.

Now I can concentrate on writing the next article instead of remembering to update another page on my website.

Leave a Reply

Your email address will not be published. Required fields are marked *