A post that used to earn 400 clicks a month is down to 130. The average position went from 8.2 to 19.4. Somebody changes the date, adds “2026” to the title, and waits. Nothing happens, because the page still answers the 2023 version of the query.
The date was never the problem. The page lost its ranking, and a date stamp does not restore a ranking. Neither does a full rewrite, which is the other reflex and a much more expensive one.
Before you touch the post, you need to know which of five things happened. Each has a different fix, and two of them should not be fixed with content at all.
The Question Is Not “Refresh or Rewrite?”
“Should I update this post or start over?” skips the diagnosis. It assumes the page declined because the content is stale, which is one of five possible causes and often not the actual one.
Google’s guidance on helpful content makes a similar point about drop audits: look at which pages were most impacted and for which types of searches before deciding what to do. That is a measurement instruction, not a writing instruction.
The five causes:
| What happened | Signal | Right response |
|---|---|---|
| Lost ranking | Average position moved down several places | Refresh the content |
| Lost CTR | Position held, click-through rate fell | Rewrite the title and description |
| Lost demand | Impressions fell across the board | Often nothing |
| Cannibalization | Two of your URLs swap places for one query | Consolidate |
| Indexing failure | Page disappeared from the export | Fix indexing, not prose |
Only the first one is a content problem in the usual sense. The second is a snippet problem. The third may be a market problem. The fourth is an architecture problem. The fifth is a technical problem that looks like a content problem because the symptom is missing traffic.
Five Ways a Page Can Decline
Lost ranking. The page still appears for the same queries, just lower. Impressions may hold or fall slightly; position moves down. This is the case where a refresh earns its keep, because something outranks you that did not before.
Lost CTR. Impressions hold, position holds, clicks fall. The page is being seen and skipped. That is a title, description, or SERP-feature problem. Rewriting the body will not fix a title that reads like every other result.
Lost demand. Impressions fall, position barely moves, clicks fall with impressions. Fewer people are searching. A refresh cannot manufacture demand, and a rewrite definitely cannot.
Cannibalization. Two pages on your site compete for one query and alternate positions. Page-level reporting hides this because each URL looks mediocre rather than broken.
Indexing failure. The URL stops appearing in the export entirely, or appears with a fraction of its former impressions. The content may be fine and unreachable.
Get Two Clean Windows Out of Search Console
The comparison needs two equal-length windows, ideally year over year so seasonality cancels out. A 28-day window against the same 28 days last year is enough for a first pass.
Open the Performance report, switch to the Pages tab, set the first window, and export. Repeat for the second window. The default view covers three months, so change the dates explicitly rather than trusting the default.
Two rules make the comparison usable:
- Same length, same filters. A 90-day window against a 28-day window produces a fake decline. Keep search type and country filters identical.
- Export both, do not eyeball the chart. The chart aggregates the whole property. The decision is per URL.
If you want this on a schedule instead of by hand, the Search Analytics API returns the same rows as JSON. Group by page, set a date range, and page through results with startRow — rowLimit accepts 1 to 25,000 rows per request and defaults to 1,000.
The API returns rows sorted by clicks, descending. Anything below the row limit is invisible, so a small page that fell off the list looks like it vanished. Export enough rows to cover your site, or filter by page.
A Comparator That Names the Problem
Reading two exports side by side by hand works for ten pages and fails at two hundred. This script joins the exports on the URL path and assigns one of the five labels.
<?php
/**
* triage.php — classify Search Console page declines.
* Usage: php triage.php before.csv after.csv
*/
const MIN_CLICKS = 50; // ignore pages with fewer baseline clicks
const DEMAND_DROP = -0.25; // impressions change counted as demand loss
const POSITION_SLIP = 3.0; // worse average position counted as ranking loss
const CTR_DROP = -0.20; // relative CTR change counted as snippet loss
function load_rows( string $file ): array {
$fh = fopen( $file, 'r' );
if ( ! $fh ) {
fwrite( STDERR, "Cannot open $file\n" );
exit( 1 );
}
$header = array_map(
fn( $h ) => strtolower( trim( $h ) ),
fgetcsv( $fh )
);
$map = [];
foreach ( $header as $i => $name ) {
if ( str_contains( $name, 'page' ) || str_contains( $name, 'url' ) ) {
$map['page'] ??= $i;
} elseif ( str_contains( $name, 'click' ) ) {
$map['clicks'] = $i;
} elseif ( str_contains( $name, 'impression' ) ) {
$map['impressions'] = $i;
} elseif ( str_contains( $name, 'ctr' ) ) {
$map['ctr'] = $i;
} elseif ( str_contains( $name, 'position' ) ) {
$map['position'] = $i;
}
}
foreach ( [ 'page', 'clicks', 'impressions', 'ctr', 'position' ] as $need ) {
if ( ! isset( $map[ $need ] ) ) {
fwrite( STDERR, "Missing column: $need in $file\n" );
exit( 1 );
}
}
$rows = [];
while ( ( $row = fgetcsv( $fh ) ) !== false ) {
if ( count( $row ) <= $map['position'] ) {
continue;
}
$url = normalize_url( $row[ $map['page'] ] );
if ( '' === $url ) {
continue;
}
$rows[ $url ] = [
'clicks' => (int) $row[ $map['clicks'] ],
'impressions' => (int) $row[ $map['impressions'] ],
'ctr' => (float) rtrim( $row[ $map['ctr'] ], '%' ) / 100,
'position' => (float) $row[ $map['position'] ],
];
}
fclose( $fh );
return $rows;
}
function normalize_url( string $url ): string {
$url = trim( $url );
if ( '' === $url ) {
return '';
}
$parts = parse_url( $url );
if ( ! $parts || ! isset( $parts['host'] ) ) {
return '';
}
$path = $parts['path'] ?? '/';
if ( '/' !== $path ) {
$path = rtrim( $path, '/' );
}
return strtolower( $parts['host'] ) . $path;
}
function pct( float $before, float $after ): float {
if ( 0.0 === $before ) {
return 0.0;
}
return ( $after - $before ) / $before;
}
function classify( array $b, ?array $a ): array {
if ( null === $a ) {
return [ 'indexing', 'Inspect URL: noindex, canonical, or removal' ];
}
$d_clicks = pct( (float) $b['clicks'], (float) $a['clicks'] );
$d_impressions = pct( (float) $b['impressions'], (float) $a['impressions'] );
$d_position = $a['position'] - $b['position'];
$d_ctr = pct( $b['ctr'], $a['ctr'] );
if ( $d_impressions <= DEMAND_DROP && $d_clicks <= DEMAND_DROP && $d_position < POSITION_SLIP ) {
return [ 'lost_demand', 'Leave unless the demand is permanent' ];
}
if ( $d_position >= POSITION_SLIP ) {
return [ 'lost_ranking', 'Refresh: add depth, sources, current facts' ];
}
if ( $d_impressions > DEMAND_DROP && $d_position < POSITION_SLIP && $d_ctr <= CTR_DROP ) {
return [ 'ctr_loss', 'Rewrite the title and description, not the body' ];
}
if ( $d_clicks <= -0.30 ) {
return [ 'unexplained', 'Check queries and SERP features for this URL' ];
}
return [ 'stable', 'Leave' ];
}
$before = load_rows( $argv[1] );
$after = load_rows( $argv[2] );
$rows = [];
$counts = [];
foreach ( $before as $url => $b ) {
if ( $b['clicks'] < MIN_CLICKS ) {
continue;
}
$a = $after[ $url ] ?? null;
[ $class, $action ] = classify( $b, $a );
if ( 'stable' === $class ) {
continue;
}
$counts[ $class ] = ( $counts[ $class ] ?? 0 ) + 1;
$rows[] = [
'url' => $url,
'clicks' => $b['clicks'] . ' -> ' . ( $a['clicks'] ?? 0 ),
'impressions' => sprintf(
'%+d%%',
(int) round( pct( (float) $b['impressions'], (float) ( $a['impressions'] ?? 0 ) ) * 100 )
),
'position' => null === $a ? 'gone' : sprintf( '%.1f -> %.1f', $b['position'], $a['position'] ),
'class' => $class,
'action' => $action,
];
}
usort( $rows, fn( $x, $y ) => strcmp( $x['class'], $y['class'] ) );
printf(
"%-46s %-13s %8s %-14s %-13s %s\n",
'URL',
'Clicks',
'Impr',
'Position',
'Class',
'Action'
);
foreach ( $rows as $r ) {
printf(
"%-46s %-13s %8s %-14s %-13s %s\n",
substr( $r['url'], 0, 46 ),
$r['clicks'],
$r['impressions'],
$r['position'],
$r['class'],
$r['action']
);
}
fwrite( STDERR, "\n" );
foreach ( $counts as $class => $n ) {
fwrite( STDERR, sprintf( "%s: %d\n", $class, $n ) );
}
fwrite( STDERR, sprintf( "pages judged: %d\n", count( $before ) ) );
Run it with PHP 8 or newer, since it uses str_contains and the null-coalescing assignment operator:
php triage.php pages-before.csv pages-after.csv
Output from a test run against fixtures built to hit every branch:
URL Clicks Impr Position Class Action
wpfrank.com/ctr-drop 900 -> 480 +1% 3.1 -> 3.3 ctr_loss Rewrite the title and description, not the body
wpfrank.com/missing-page 250 -> 0 -100% gone indexing Inspect URL: noindex, canonical, or removal
wpfrank.com/demand-drop 300 -> 90 -74% 6.4 -> 6.6 lost_demand Leave unless the demand is permanent
wpfrank.com/ranking-drop 400 -> 130 -2% 8.2 -> 19.4 lost_ranking Refresh: add depth, sources, current facts
wpfrank.com/slow-erosion 300 -> 205 -17% 12.0 -> 12.1 unexplained Check queries and SERP features for this URL
The four thresholds at the top are the only judgement calls in the script, and they are meant to be edited. A site whose average position normally swings two spots between quarters needs a larger POSITION_SLIP. A site with a long tail needs a smaller MIN_CLICKS than 50, or it will judge almost nothing.
The header matcher is loose on purpose: it looks for the words “page”, “clicks”, “impressions”, “ctr”, and “position” anywhere in the header row, so small differences in export layout do not break it.
Reading the Output
A row classified lost_ranking is the one you were planning to fix. Position moved down by three or more while impressions held. Something changed above you, or the page no longer matches the query as well as it did.
A ctr_loss row is cheaper to fix and more often misdiagnosed. The page still ranks. It is being passed over. That is a title problem, not a depth problem.
A lost_demand row is the one people rewrite by mistake. Impressions fell 25% or more and position barely moved. The audience shrank. Rewriting the post produces a better page for a smaller audience.
An indexing row outranks everything else on the list. Do not write a word until you know why the URL left the index.
An unexplained row means the script ran out of explanations. Clicks fell more than 30% without a clean position shift or a clean impressions drop. Look at the queries tab for that URL before doing anything.
Treat the list as a queue, not a report. The top item is the one with the clearest cause and the highest click loss, not the oldest post.
What “Refresh” Actually Means
A refresh keeps the URL, the angle, and the structure, and fixes what is factually or competitively out of date. It is the right move when the page still matches the query but has fallen behind.
Working through a refresh, in order:
- Check the facts that expire. Prices, version numbers, plugin names, screenshots, dated recommendations. A tutorial that references a settings screen that no longer exists has a credibility problem before it has a ranking problem.
- Look at what now outranks you and note the specific gap. Not “they have more words”. The gap is usually a worked example, a current screenshot, a comparison table, or a question answered directly that your page answers obliquely.
- Add the missing section, do not pad the existing ones. Supplementary content is the useful frame here: add the part that was missing, not 400 more words of restatement.
- Fix internal links in and out. A refresh is the natural moment to repair outbound links that have rotted. The dead link checker guide covers finding and fixing them at scale.
- Update the modified date honestly. If the substance did not change, the date should not change either. Changing a date without changing the page is the move that started this article.
A refresh is usually a two-hour job. Keep it that size. When it grows past a day, you are rewriting, and you should admit that to yourself before you start.
When a Rewrite Is the Right Call
A rewrite is correct in three situations, and only three:
- The intent changed. The query now means something different than it did when the post was written. A “best X” query that has shifted toward comparison and pricing needs a different page, not a better-worded old one.
- The structure fights the answer. The page buries its answer under eight paragraphs of background, so it loses position to pages that answer in the first screen. Bolting an answer onto the top of a badly structured page produces a page with an answer bolted onto the top.
- The page is wrong at the foundation. A tutorial written for a workflow that no longer exists cannot be refreshed into a correct one. Replace it, and redirect if the URL changes.
Everything else is a refresh. The reason to prefer refresh is not effort: it keeps the URL, and with it whatever links, history, and internal equity the page has accumulated. A rewrite that ships to a new URL throws that away and starts from zero.
If the rewrite is genuinely structural, semantic SEO and entity coverage is the useful lens: decide which entities and subtopics the page must cover, then write to that list instead of rewriting by feel.
CTR Loss: Fix the Snippet First
When the comparator says ctr_loss, resist the urge to touch the body. The body is ranking. The listing is the problem.
Google’s title link documentation explains that Google chooses the title link from several sources, and that you can influence the choice with a descriptive, concise <title>. Two practical consequences:
- Your
<title>may not be the title in the results. If Google rewrites it, the rewrite is a signal that your title was vague, duplicated, or padded. Fix the title rather than the page. - The description is not a ranking factor and still matters. It is the pitch. If it reads like the first sentence of the post, it wastes the slot.
Test one change at a time. Changing the title and the description and the featured image in the same week tells you nothing about which one worked. Two weeks per change is usually enough to see a CTR move on a page with real impressions.
Indexing Failure Is Not a Content Problem
If a URL disappears from the export, the first hypothesis should be technical, not editorial. The page may have picked up a noindex, lost a canonical fight, been folded into another URL, or been removed by a migration.
Use URL Inspection to see the indexed version, the Google-selected canonical, and the rendered HTML. Two details matter:
- The live test does not predict the canonical. Google’s documentation is explicit that you can only determine the canonical from the indexed data. A live test that looks perfect proves indexability, not indexation.
- The rendered version is what counts. If the content you see in the browser is injected by JavaScript that the crawler cannot run, the indexed page is empty and the export will show the consequence.
Only after indexing is confirmed should the page join the content queue.
Cannibalization: The Case the Export Cannot See
Two of your pages competing for one query can look like two mediocre pages. Neither has crashed; both are slightly worse than they would be alone. The page-level comparator cannot detect this because it has no query dimension.
To find it, export the Queries dimension for the same two windows, filtered to the pages you care about, and look for one query appearing against two URLs. Then compare dates: if the two URLs have swapped positions over time, they are splitting the same intent.
The fix is a decision, not an edit:
- Merge when both pages answer the same question. Pick the stronger URL, absorb the useful parts of the other, and redirect.
- Differentiate when they answer adjacent questions. Rewrite the weaker one around the distinct intent and link the two.
- Leave when the overlap is a brand term or a query where your own pages legitimately serve different stages.
Page structure decides whether this recurs. The guide to structuring WordPress pages for SEO is the place to fix the pattern rather than the individual pair.
Consolidation: Two Pages, One Intent
Consolidation is the cheapest content win available, because it removes work instead of adding it. The recipe:
- Choose the URL with more clicks, better links, or the cleaner slug.
- Move the unique, useful sections from the weaker page into it.
- Redirect the weaker URL with a 301.
- Update internal links that pointed at the old URL.
- Keep the redirect in place. Removing it later restores the split.
The thing to avoid is merging two thin pages into one slightly less thin page. If neither page had a distinct answer, the merged page has to answer the query better than either did, or the redirect just moves the problem.
Lost Demand: Sometimes the Answer Is Nothing
Demand loss is the classification that saves the most wasted effort, and it is the hardest one to accept.
If impressions fell 40% because a tool was discontinued, a trend passed, or a season ended, no amount of writing brings the traffic back. The page can still be worth keeping for the people who do search, especially if it is evergreen in the sense of remaining accurate. The guide to evergreen content is useful for telling the difference between a page whose demand expired and a page whose accuracy expired.
Before you rewrite, check one thing: does the page still convert or assist for the traffic it does get? A page at a third of its old traffic that still produces leads is doing its job. A page at a third of its traffic that never converted is a candidate for consolidation or removal, not for a rewrite.
Leaving a Page Alone Is a Decision
Most pages on a mature site should not be touched this quarter. That is not neglect; it is triage.
The comparator deliberately skips anything classified stable, and a site with 200 posts usually produces a flagged list of 15 to 30. Working through that list in order of click loss is a realistic quarter. Rewriting all 200 is not a plan, it is a way to break the 170 that were working.
The second reason to leave pages alone: every change resets your ability to attribute. If you edit forty pages in a month and traffic moves, you have learned nothing about which edit mattered.
Measure the Result Without Fooling Yourself
A refresh is an experiment with a bad control group, so make the comparison as honest as you can.
- Record the baseline before the edit. Clicks, impressions, CTR, position for that URL, same window length, exported.
- Wait for the crawl. A change published today can be assessed in two to four weeks; judging it at day three measures noise.
- Compare the page to itself, not to the site. Site-wide traffic includes seasonality, other pages, and campaigns. The question is whether this URL moved relative to its own baseline.
- Expect position to move before clicks. A ranking gain shows up in position first and clicks second, especially on low-volume queries.
- Write down the verdict. “Refreshed, no change, position stable” is a result. Without it, the same page gets refreshed again next quarter.
If a refresh produces no movement after a month, the honest conclusion is that the content was not the constraint. That points back to the diagnosis: check whether the query now demands a different page type, whether a competitor has a structural advantage, or whether the page is being outranked by a result type you cannot match. The factors outside the body copy are covered in the guide to improving SEO rankings.
The Order of Operations
- Export two equal windows from the Pages tab, or pull them from the Search Analytics API.
- Run the comparator and read the class column, not the click delta.
- Fix
indexingfirst. Nothing else matters until the URL is indexable and indexed. - Fix
ctr_lossnext. It is the cheapest change on the list. - Refresh
lost_rankingpages whose intent is unchanged. - Rewrite only where the intent, structure, or foundation is wrong.
- Consolidate overlapping pairs, then redirect and repair internal links.
- Leave
lost_demandpages unless they still convert. - Record every verdict so the next audit starts from evidence.
The date stamp is the last thing you should change, and often the only thing you should not. Ranking is what the page earned; the date is what someone typed into a field.
FAQ
Should I update the date on an old blog post?
Only when the content changed. Changing the date without changing the substance is a visible signal that the page was not actually updated, and it does not restore a lost ranking. If the page needs to look current, make it current.
How do I know if I should refresh or rewrite a post?
Compare two Search Console windows for that URL. If the average position fell while impressions held, refresh. If the position held and CTR fell, rewrite the title and description. If impressions fell and position held, the demand changed and content will not fix it.
How often should I refresh old blog posts?
Refresh the pages the data flags, not the pages that are old. A calendar-driven refresh schedule spends effort on pages that are working. Run the comparison quarterly and work the list by click loss.
Does refreshing content hurt SEO?
Not by itself. The risks are changing the URL, removing content that other pages link to, and changing too many variables at once so you cannot tell what worked. Keep the URL and change one thing at a time.
What is content decay?
Content decay is the gradual loss of traffic to a page as its ranking slips, its facts age, or demand falls. It is not one phenomenon, which is why the fix depends on which of those three is happening.
Can I just delete old blog posts?
Sometimes, but deletion is the most destructive option. A page with links and history is worth redirecting rather than deleting. Delete only when the page has no links, no traffic, and no unique information, and redirect the URL anyway.
How long does a refresh take to show results?
Usually two to four weeks before you can read the trend, and longer for low-volume pages. Position often moves before clicks. Judging a change after three days measures normal fluctuation.
What if the page has no impressions at all?
That is an indexing or demand question, not a content question. Inspect the URL, confirm it is indexed, then check whether any query exists for it. A page nobody searches for does not need a rewrite.
Leave a Reply