From Crisis To Success - How We Transformed Whatsoninvers Site's Performance
![Fast speed vs slow speed image](/images/slow.webp)
![James Tucker Author](/images/james-tucker.webp)
James Tucker
Fri Sep 22
When Mike’s local news website Whats On Invers started crashing under the weight of just 15 readers trying to access it simultaneously, he knew his business was in trouble. For a news site meant to serve breaking local stories to hundreds of readers at once, this wasn’t just a technical glitch—it was a serious threat to his business’s survival. This case study shows how we transformed Whats On Invers from a struggling website that turned away readers into a high-performance digital platform. The result? A lightning-fast news site that easily handles 2000+ simultaneous readers and achieved a near-perfect Google performance score, jumping from 25% to 99%
The Business Problem
Imagine running a restaurant that could only serve 15 customers at a time, then having to turn away hundreds during the dinner rush. That was essentially Mike’s situation. His website would completely fail just when he needed it most - during breaking news when 500+ readers would try to access the site simultaneously.
The previous developers had left him with a website that was:
- Unreliable during peak traffic
- Frustratingly slow to load
- Losing readers to technical issues
- Putting his advertising revenue at risk
The Solution: A Two-Phase Approach
Our initial challenge was to save the website quickly while minimizing business disruption. While our long-term vision was to move away from WordPress completely (which we’ve since accomplished), making such a dramatic change immediately would have been too risky for an active news platform. We will discuss how we implemented phase one in this article.
Phase 1: Rapid Performance Recovery
We kept the existing WordPress backend that Mike’s team was familiar with, but completely rebuilt the customer-facing part of the website using modern technology. This hybrid approach gave us the best of both worlds - familiar content management for the staff and dramatically improved performance for readers.
Objectives
- Significantly decrease the number of installed plugins without causing any disruptions to the website’s functionality.
- Develop a high-performance, mobile-first front-end capable of handling 1000+ concurrent users.
- Preserve the core advertising functionality from the original website, this includes specific rules about ad placement within content.
- Handle sponsors for taxonomies, posts. Also the requirement that a different category of adverts be served to the funeral notices posts.
Creating a functional performant front-end with all the required specifications was a big task complicated by the fact that not all the data was served by the Wordpress REST API. We had to get my hands dirty and build a custom wordpress plugin which would expose this data.
Challenges
Below is a by no means comprehensive list of challenges faced in the implementation of the website.
1. Dynamically loading adverts within content
-
The challenge here is the fact that a performant news website needs to cache as much content as possible. The old wordpress implementation would build the webpage on the server and then send that webpage to the user as the response, and this included with adverts and any other content (we talk about this more in our article about Static vs Dynamic Websites). How could we replicate this behaviour in a headless fashion?
-
Solution: inject placeholders into raw content based on the number of paragraphs.
function inject_placeholders_into_content($post_content, $post_categories) {
$paragraphs = explode('</p>', $post_content);
$paragraph_count = count($paragraphs);
// Determine index based on paragraph count
if ($paragraph_count >= 15) {
$indexes = [4, 15];
} elseif ($paragraph_count >= 10) {
$indexes = [3, 10];
} else {
$indexes = [2, 6];
}
// Predefined insertion points
$insertionPoints = [2, 6, 10, 15];
for ($i = 0; $i < $indexes[0]; $i++) {
// Use the last value if the index is out of bounds
$insert_after = $insertionPoints[$i] ?? $insert_after;
$pos = $i + 1;
$insertion = "<div class='g g-1'><div class='g-single a-slot-" . $pos . "'>
<div class='text-center advertise'><small class='advertise-text color-silver-light'>
– <a href='/promote/' rel'noopener' target='_blank'>Advertise on whatsoninvers.nz</a>
–</small></div><div class='a-" . $pos . "'></div></div></div>";
array_splice($paragraphs, $insert_after, 0, $insertion);
}
$post_content = implode('', $paragraphs);
return [$post_content, $indexes[0]];
}
This function was used within the context of another function as defined below:
function serve_posts($request)
{
$input_params = $request->get_params();
$request = new WP_REST_Request('GET', '/wp/v2/posts');
// Define a list of parameters that should be cast to int
$intParams = ['categories', 'id', 'author', 'page', 'tags', 'per_page'];
foreach ($input_params as $key => $value) {
if (in_array($key, $intParams)) {
$request->set_param($key, (int)$value);
} elseif ($key === 'slug') {
$request->set_param($key, $value);
}
}
// Set default value for per_page if not provided
if (!isset($input_params['per_page'])) {
$request->set_param('per_page', 24);
}
$request->set_headers([
'Content-Type' => 'application/json',
'Cache-Control' => 'max-age=900, stale-while-revalidate=600, s-maxage=1800',
'access-control-allow-headers' => 'X-WP-Total, X-WP-TotalPages',
'access-control-expose-headers' => 'X-WP-Total, X-WP-TotalPages, Link'
]);
$response = rest_do_request($request);
$posts = rest_get_server()->response_to_data($response, true);
$data = [];
$i = 0;
foreach ($posts as $post) {
$obj = inject_placeholders_into_content($post['content']['rendered'], $post['categories']);
$post['content']['rendered'] = $obj[0];
$post['num_ads'] = $obj[1];
$data[$i] = $post;
$i++;
}
$client_response = new WP_REST_Response($data);
$client_response->header('X-WP-Total', $response->get_headers()['X-WP-Total']);
$client_response->header('X-WP-TotalPages', $response->get_headers()['X-WP-TotalPages']);
$client_response->set_status(200);
return $client_response;
}
The main point here is that Wordpress would internally call the /wp/v2/posts
endpoint (with any required parameters) while also attempting to cache that response to reduce server load. We are able to then inject placeholders within the content and then serve them to the front-end, where the front-end will scan the content for these placeholders and replace them with the required adverts. What’s important to note is this:
$response = rest_do_request($request);
$posts = rest_get_server()->response_to_data($response, true);
Additionally, response_to_data()
allows us to append the _embedded field to each post object which contains the featured image for each post, as well as other key information required by the front end. By default the endpoint wp/v2/posts
does not include this information.
With all that being said, it is important to use the add_action
function to register the rest route so we are able to access this route from our client.
add_action('rest_api_init', function () {
register_rest_route('wp/v2', 'posts_filter', [
'methods' => 'GET',
'callback' => 'serve_posts',
'args' => [
'id',
'category',
'slug',
'author',
'page',
'per_page',
'tags'
],
'permission_callback' => '__return_true'
]);
}
2. Dynamically replacing the placeholders on the front-end
- With the above implementation we have placeholders within the content where we’d like the adverts to be, however the question remains how do we actually inject these adverts dynamically into the content?
Solution:
useEffect(() => {
const getPostBlockAds = async () => {
let ads = await getBlockAds(libraries.source.api);
const funeralAds = ads.filter((ad => ad.funeral == true));
ads = formatBlockAds(ads);
let blocks = new Array();
// inject into content.
for (let i = 0; i < post.numAds; i++) {
blocks[i] = ads[i];
let injectContent;
if (ads[i].isIframe) {
injectContent = `<iframe src="${ads[i].src}"
style="border:none overflow:hidden" width="${ads[i].width}"
height="${ads[i].height}" frameborder=0>`;
} else {
injectContent = `<a href="${ads[i].href}"><img src="${ads[i].src}" /></a>`;
}
let searchString = `<div class='a-${i+1}'></div>`;
tempPostContent = tempPostContent.replace(searchString, injectContent);
}
if (isMounted) setPostContent(tempPostContent);
}
}, [post.id]);
The above snippet utilizes the useEffect
React Hook which allows it to execute the getPostBlockAds()
function after the component has rendered thus minimizing load time. The significance of useEffect
in this context is to ensure that the code inside it is executed after the component is rendered and whenever the post.id dependency changes. This ensures that the ad fetching and injection process occurs when the post.id changes, indicating a new post has been loaded or the post content has been updated.
A loop within getPostBlockAds()
first fetches the adverts (via a custom Wordpress REST API endpoint /wp/v2/serve_ads
), randomly shuffles the adverts via formatBlockAds()
, then iterates over the ads and injects them into the content of the post, and depending on whether the ad is an iframe or an image, appropriate HTML content is generated for injection. Finally if the component is still mounted via isMounted, the post content is updated with the injected ads using setPostContent, which is defined using React.useState([])
hook.
How postContent
is used:
<Content
as={Section}
px={{ base: "0", md: "0" }}
size="md"
pt={{base: "25px", md: "30px"}}
color={mode('white', 'gray.600')}
>
<Html2React html={postContent} />
{sponsor.news_sponsor_word && (
<Section p="20px" mt="20px" key={post.id} bg={mode('gray.100', 'gray.300')}>
<Heading size="md" fontWeight="black" fontSize='xl' as="h3" pb="10px">
A message from our sponsor
</Heading>
<Text>{sponsor.message}</Text>
<Heading size="md" fontWeight="bold" as="h4" pb="10px" pt="10px">Contact</Heading>
<Text>Phone: {sponsor.telephone}</Text>
<Text>Website: <Link link={sponsor.website} color="blue">{sponsor.website}</Link></Text>
</Section>
)}
</Content>
Technology Stack
- Frontend: Frontity, React.js, Chakra UI, Emotion CSS styling.
- Backend: Wordpress CMS leveraging the Wordpress REST API.
The front-end client is deployed on Vercel.
Outcome
Not only has the website’s performance seen a significant boost, but its appearance, especially on mobile devices, has also undergone notable enhancement.
![](/_astro/woi-compare.BM5toQ41_Z2krT9a.webp)
![](/_astro/source_development.CJVLwYmx_Z20TIDu.webp)
Going from an F grade at a 25% performance score to an A grade with a 99% performance score is an enormous jump.