Mert Özen Further With Every Line
SEO & GEO Content SEO — #07

Content Maintenance: Content Decay, the Update–Merge–Delete Decision and Keeping Version-Dependent Posts Alive

Mert Özen Nov 14, 2026 12 min 1 views
Content Maintenance: Content Decay, the Update–Merge–Delete Decision and Keeping Version-Dependent Posts Alive

Content decay is the decline of a page's search traffic over months, and the fix is not updating the date but separating the cause and deciding to update, rewrite, merge or remove. We cover detecting decay with year-over-year comparisons in Search Console, extracting signals with .NET 10, keeping version-dependent posts alive as .NET 11 ships, using dateModified honestly, spam policy boundaries and measuring with annotations.

Version note: The code in this post was built and run with .NET 10; the post is based on the November 2026 state of Google's documentation on traffic drops, publication dates, sitemaps and spam policies, and on the .NET 11 release that became generally available on 10 November 2026.

Guess First

.NET 11 shipped on 10 November 2026. The Bookstore blog has hundreds of posts written with .NET 10, and clicks on the EF Core migration guide are down 35% compared with the same period last year. Two proposals come from the team: (1) change ".NET 10" to ".NET 11" in the titles of all posts and move their dates to today; (2) delete the posts whose traffic is falling in bulk, so the "site gets cleaned up". Which of these proposals will bring the guide's traffic back? Which one will harm the site as a whole? And is the 35% drop really caused by the content getting old?

What Is Content Decay and How Do You Detect It?

Content decay is the slow decline of a page's search traffic over months. On a technical blog it has four common causes: interest in the query moves elsewhere (readers now search for the new release, not the old one), more current or better pages move ahead, the results page changes (an AI Overview or a video box reduces clicks), or the page's content goes stale (commands, APIs and screen names have changed).

Not every drop is decay. Google's documentation on debugging search traffic drops lists other causes too, and most of them belong to the non-content side of SEO: algorithm updates, crawling and indexing problems, security issues, spam violations, seasonality, site moves and reporting glitches. So the first step is to separate whether the drop belongs to the page or the site, and whether it is seasonal.

The method the documentation recommends for Search Console is the basis of this post too. First look at the 16-month range to see whether the drop repeats in the same period every year. Then compare the last three months with the previous period or with the same period last year; the latter is more reliable for removing seasonality. Sort the Pages table by the difference in clicks and start with the pages that lost the most. Look at the chart as well: if impressions and clicks fell together, the problem is visibility; if only clicks fell, the documentation says your titles and snippets may have become less appealing or competitors may be showing richer results. Finally, check in Google Trends whether interest in the query has dropped in general.

Extracting Signals in Code

Instead of doing this comparison by hand for every page each month, let's write a small program that puts two periods side by side. The input is two periods aggregated per page from the bulk export: August–October of last year (prev) and the same months this year (curr). Since Search Console data goes back 16 months, this range is available. The rows are sample data prepared for the Bookstore blog, not a real site's numbers.

url,period,impressions,clicks,sum_position
/blog/ef-core-migration-rehberi,prev,5200,410,16640
/blog/ef-core-migration-rehberi,curr,5050,265,34845
/blog/ef-core-8-yenilikleri,prev,3900,350,6240
/blog/ef-core-8-yenilikleri,curr,1150,96,2415
/blog/dotnet-6-minimal-api,prev,2600,190,11440
/blog/dotnet-6-minimal-api,curr,480,11,5616
using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
string path = args.Length > 0 ? args[0] : "gsc-pages-yoy.csv";

// url,period,impressions,clicks,sum_position — period: "prev" (last year) or "curr" (this year), same months
Dictionary<string, Dictionary<string, PeriodStats>> pages = (await File.ReadAllLinesAsync(path))
    .Skip(1)
    .Select(line => line.Split(','))
    .GroupBy(f => f[0])
    .ToDictionary(g => g.Key, g => g.ToDictionary(f => f[1], f => new PeriodStats(int.Parse(f[2]), int.Parse(f[3]), long.Parse(f[4]))));

Console.WriteLine($"{"url",-36}{"clicks",-10}{"Δ",6}{"impr Δ",8}  {"position",-12}signal");
foreach ((string url, Dictionary<string, PeriodStats> p) in pages.OrderBy(x => x.Key))
{
    PeriodStats curr = p["curr"];
    if (!p.TryGetValue("prev", out PeriodStats? prev))
    {
        Console.WriteLine($"{url,-36}{"-> " + curr.Clicks,-10}{"",6}{"",8}  {"-> " + curr.Position.ToString("F1"),-12}NEW (no baseline)");
        continue;
    }

    double clicks = Change(prev.Clicks, curr.Clicks);
    double impressions = Change(prev.Impressions, curr.Impressions);
    double positionDelta = curr.Position - prev.Position;

    // A signal is not a decision; it only tells you which question to ask
    string signal = (clicks, impressions, positionDelta) switch
    {
        _ when curr.Impressions < 100 && prev.Impressions < 100 => "LOW VISIBILITY -> merge or remove?",
        ( > -0.2, _, _) => "ok",
        (_, <= -0.5, _) => "DEMAND DROP -> query interest moved?",
        (_, _, >= 2) => "RANKING DROP -> refresh?",
        _ => "CTR DROP -> check the SERP",
    };
    Console.WriteLine($"{url,-36}{$"{prev.Clicks}->{curr.Clicks}",-10}{clicks * 100,5:+0;-0}%{impressions * 100,7:+0;-0}%  {$"{prev.Position:F1}->{curr.Position:F1}",-12}{signal}");
}

static double Change(int before, int after) => (after - before) / (double)before;

record PeriodStats(int Impressions, int Clicks, long SumPosition)
{
    public double Position => (double)SumPosition / Impressions + 1;
}

Output of dotnet run:

url                                 clicks         Δ  impr Δ  position    signal
/blog/aspnet-core-di-lifetime       300->176    -41%     +4%  3.0->3.1    CTR DROP -> check the SERP
/blog/cs8618-nullable-uyarisi       520->690    +33%    +24%  2.4->2.0    ok
/blog/dependency-injection-nedir    640->655     +2%     +4%  4.0->3.8    ok
/blog/dotnet-6-minimal-api          190->11     -94%    -82%  5.4->12.7   DEMAND DROP -> query interest moved?
/blog/ef-core-10-yenilikleri        -> 310                    -> 2.5      NEW (no baseline)
/blog/ef-core-8-yenilikleri         350->96     -73%    -71%  2.6->3.1    DEMAND DROP -> query interest moved?
/blog/ef-core-migration-rehberi     410->265    -35%     -3%  4.2->7.9    RANKING DROP -> refresh?
/blog/kitap-onerileri-2023          3->1        -67%    -43%  18.0->22.5  LOW VISIBILITY -> merge or remove?

The program does not decide; it tells you which question to ask for each page. Let's read the rows:

ef-core-migration-rehberi: impressions are almost the same, position fell from 4.2 to 7.9 and clicks are down 35%. Readers still search for the same thing but other pages have moved ahead. A classic "update" candidate: are the page's commands and examples current, and do competing pages have something this page lacks?

aspnet-core-di-lifetime: position and impressions held, clicks fell 41%. This is not decay but a change in the results page. The query needs a look in a private window: has an AI Overview started appearing, have competitors got a richer appearance, or has our title gone stale?

ef-core-8-yenilikleri and dotnet-6-minimal-api: impressions fell 70–80%. Readers no longer search for these versions. The two pages are in different situations, and we deal with each below under version-dependent content.

kitap-onerileri-2023: under 100 impressions in both periods. This is the thin page we saw in the fourth post of the series; a candidate for merging or removal.

ef-core-10-yenilikleri: a new page with no data from last year; for comparison you need to wait a year or use the first three months after publication as the baseline.

The Decision Tree: Update, Rewrite, Merge, Remove

When moving from signal to decision, ask the same questions for every page: Does the reader's question on this page still exist? Does the page answer it correctly today? Is there another page on the site that answers the same question? Depending on the answers, four options remain:

SituationDecisionTechnical step
The question exists, the page is partly outdatedUpdate: refresh commands, examples, screen namesSame URL; a visible "Updated" date and dateModified
The question exists, the page's approach is now wrongRewriteSame URL, new content; state the change clearly on the page
A better page answers the same questionMergeMove the content to the stronger page and 301-redirect the old URL
The question is gone and so is any equivalent pageRemove404 or 410; clean up links on the site

Removal is the last option. We covered the difference between 404 and 410 and how Google interprets them in the Technical SEO series' HTTP status code post, and setting up a 301 correctly in its redirects post. Bookstore's second proposal, deleting posts with falling traffic in bulk, means applying the last option of this tree to every page without asking a single question.

Keeping Version-Dependent Posts Alive

The most specific maintenance problem of a technical blog is versions. mertozen.com uses a rule for this: in technology series, "core" posts on a stable base version come first, and what later versions bring is covered in separate "what's new in each release" posts. Today the base is .NET 10, an LTS release supported until November 2028. .NET 11 shipped on 10 November 2026 and, as an STS release, gets two years of support. Under this rule, the arrival of .NET 11 does not mean rewriting hundreds of core posts; it means new posts that cover what .NET 11 brings and, where needed, a paragraph and a link in the core posts saying "this changed in .NET 11".

This model produces two kinds of pages, and they are maintained differently. A core post has no version in its title and its query is lasting ("ef core migration nasıl yapılır"); these pages are kept alive by updating them. A version post has the version in its title and its query is tied to that version ("ef core 8 yenilikleri"); these pages stay as the record of that version. Let's read the two drops in the example through this distinction:

ef-core-8-yenilikleri: support for .NET 8 and EF Core 8 ended on 10 November 2026, and readers now search for EF Core 10. The page is not wrong, just historical. The right step is neither to delete it nor to change its title to "EF Core 10"; it is to add a short note at the top linking to the current version post and leave the page as a record. Changing the version and adapting the content to the next release loses the few readers still looking for the old version and makes two version pages compete for the same query.

dotnet-6-minimal-api: this is a different case. Support for .NET 6 ended in November 2024, and the page is not really a version post but a core post that put the version in its title. If the site has a post covering the same topic with .NET 10, the right step is to merge and 301-redirect the old URL; if not, update the post to .NET 10. Because the URL contains the version, moving it to a new URL during the update and redirecting the old one is cleaner. This also shows why it matters to keep versions out of the URLs and titles of core posts.

The "Updated" Note and dateModified

Google's documentation on publication dates recommends a clearly labelled visible date on the page (publication date, updated date or both), datePublished and dateModified in structured data, and consistency between the two. The sitemap documentation says Google uses lastmod only if it is consistently and verifiably accurate, and that the value should reflect the last meaningful change to the main content, structured data or links; updating the copyright year in the footer is not such a change. The people-first guide we saw in the first post of the series lists changing dates without substantial changes to the content directly as a warning sign.

mertozen.com's current state is a good example here. The dateModified in the JSON-LD and the lastmod in the sitemap come from the CMS's update time, and the code guarantees that dateModified is never earlier than the publication date. But the page shows only the publication date; so readers and structured data get different information. In addition, because the CMS's update time moves on every save, fixing a typo also moves dateModified and lastmod forward. Two steps are enough for an honest setup: add a separate "content updated" field to the CMS that editors set only for substantial changes, and show it on the page with an "Updated: ..." label when it is set; feed dateModified and lastmod from this field as well.

That is why Bookstore's first proposal is wrong twice over: writing the version name into titles and moving the dates means showing freshness without changing the content, and turning core posts that are not version-dependent into version-dependent ones.

Spam Policy Boundaries

Three spam policies deserve particular attention during content maintenance, because all three can get mixed up with ideas for "reviving an old site".

Scaled content abuse. Having AI regenerate old posts in bulk and republishing them without giving readers anything new is what the policy describes: many pages that add little value, regardless of how they were produced. The measure of an update is not the number of changed words but whether the answer to the reader's question has improved.

Site reputation abuse. Publishing third-party content unrelated to the site's topic (sponsored reviews, coupon pages and the like) to exploit the reputation the site has earned. On 28 August 2026 Google changed how this policy is enforced in the EEA: for users in the EEA the effect of the manual action does not apply, and the affected section may be separated so that over time it ranks independently of the rest of the site; the definition of the policy did not change. When a technical blog accepts "guest posts", the question is whether the content serves the site's readers.

Expired domain abuse. Buying an expired domain and reusing it with low-value content to benefit from its old reputation. In a maintenance context, keep this in mind when you consider buying an old project domain to redirect it to your blog or to publish unrelated content there.

Measuring: Annotations and Before/After Comparisons

The only way to know whether an update worked is to measure it. Since November 2025 Search Console lets you add custom annotations to the Performance chart: right-click the chart and add a date and a note of up to 120 characters. Everyone with access to the property can see the note, so do not put personal information in it. Google's suggested examples include template changes, site moves and reorganising content towards a different intent.

Three rules for before/after comparisons: use periods of the same length, wait a few weeks after the change (Google's traffic documentation also says the effect of changes can take days to months), and, if possible, track similar pages you did not change as a control group over the same period. If the control group fell by the same amount, your change is not the cause of the drop. The deeper side of measurement, combining GA4 with Search Console and reading the effect statistically, is the subject of the Analytics and Measurement series being written in this round.

In Production

Put the audit on a schedule. Run the decay report once a month, and separately after major releases; keep the results in a content audit table (page, signal, decision, owner, date).

Keep your data for longer than 16 months. Store history in your own storage with the bulk export or the API, so you can see yearly comparisons and release cycles.

Protect URLs that perform well. You do not have to change the URL when you update, and most of the time you should not; if a URL change is unavoidable, ship the 301 and the internal link updates in the same deploy.

Do bulk work in batches. Apply merge or removal decisions in small groups and continue only after measuring the effect of each group.

Common Pitfalls

Updating only the date. Moving the date forward without changing the content misleads readers, breaks the consistency Google's date and sitemap documentation describe, and is one of the warning signs in the people-first guide.

Changing the URL of a well-performing page. Changing a URL that gets traffic "for a cleaner slug" causes a period of fluctuation even when the redirect is set up correctly, and the gain is uncertain.

Deleting in bulk without measuring the traffic impact. Hundreds of pages that each get little traffic can carry a serious amount of long-tail traffic together; on top of that, the internal links to these pages break.

Treating every drop as decay. Seasonality, a change in the results page or a technical problem can produce the same chart; separate the cause before deciding to "update".

A Small Experiment

★☆☆ In Search Console, compare the last three months with the same period last year and sort the Pages table by the difference in clicks. For the five pages that lost the most, did impressions, clicks or position fall?

★★☆ Add a step to ContentDecay that compares the query lists of the two periods and prints a "lost queries" list for each page (use the query and URL export from the second post of the series as input).

★★★ Keep a separate "content updated" field in your CMS or in an ASP.NET Core model: generate the "Updated" label on the page, dateModified in JSON-LD and lastmod in the sitemap from this field, and show with a test that fixing a typo does not change it.

This is where the Content SEO series ends. We decided what to write (keyword research and search intent), organised what we wrote (content architecture and internal linking), wrote pages that answer and saw how to keep them alive over time. The natural next step is to measure the effect of these decisions in more detail: the Analytics and Measurement series being written in this round covers reading Search Console and GA4 data together.