Skip to content
Back to skills

Content Decay

ASecurity

Use when: kullanıcı "içerik decay", "content decay", "tıklama düşen sayfalar", "ctr düştü", "90 gün öncesine göre azalma", "trafik kaybeden sayfalar", "decay analizi", "content_decay sheet yenile" der ya da /pseo-content-decay çağırır. Also use when: aktif projenin GSC verisi master.xlsx'te mevcut (gsc-pull önce çalışmış); 90-günlük recent + previous pencere karşılaştırması yapılacak; pillar bazlı içerik decay haritası lazım; downstream content-improve / new-content-plan skill'leri bu listeyi...

  • 27 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 2, 2026
ai-agentspythongoshellbashperformance

Works with

  • cli
  • mcp

Security analysis

A100/100

Scanned October 2, 2026

npx -y skills add popiliadam/platinum-seo-engine --skill content-decay --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Content Decay?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Content Decay
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/popiliadam-content-decay/badge)](https://www.skillsdirectory.com/skills/popiliadam-content-decay)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: content-decay
description: |
  Use when: kullanıcı "içerik decay", "content decay", "tıklama düşen sayfalar",
  "ctr düştü", "90 gün öncesine göre azalma", "trafik kaybeden sayfalar",
  "decay analizi", "content_decay sheet yenile" der ya da /pseo-content-decay
  çağırır.
  Also use when: aktif projenin GSC verisi master.xlsx'te mevcut (gsc-pull
  önce çalışmış); 90-günlük recent + previous pencere karşılaştırması
  yapılacak; pillar bazlı içerik decay haritası lazım; downstream
  content-improve / new-content-plan skill'leri bu listeyi tüketecek.
  Do not use when: quick-win opportunity scoring (quick-wins skill), GSC
  delta ingestion (gsc-pull skill), tek-URL audit (tech-audit skill),
  cannibalization analizi — ayrı discovery skill'leri. Master.xlsx
  yokken çağırma; init-project + gsc-pull önce çalışmalı (DURUR #6).
version: "1.0"
status: active
category: discovery
inputs:
  project_slug:
    type: string
    required: true
    description: "Slug; resolves projects/{slug}/master.xlsx + project.config.json."
  days_back:
    type: integer
    required: false
    default: 90
    description: "Recent window length in days; previous window has equal length."
  dimensions:
    type: array
    required: false
    description: "GSC dimensions (default: ['page'])."
outputs:
  - "master.xlsx#content_decay"
  - "outputs/reports/{date}-content-decay.md"
  - "events.jsonl"
  - "inbox/gsc/{date}-enhanced_search_analytics-decay-recent-{slug}.json"
  - "inbox/gsc/{date}-enhanced_search_analytics-decay-previous-{slug}.json"
  - "inbox/dfs/{date}-historical_rank_overview-decay-{slug}.json"
consumes:
  - "init-project:projects/{slug}/master.xlsx"
  - "gsc-pull:master.xlsx#gsc_performance"
produces:
  - "new-content-plan"
  - "drift-check"
triggers:
  manual: ["/pseo-content-decay"]
  natural_language: |
    "içerik decay", "content decay", "tıklama düşen sayfalar", "ctr düştü",
    "90 gün öncesine göre azalma", "trafik kaybeden sayfalar",
    "decay analizi", "content_decay sheet yenile"
  hooks: []
  scheduled:
    - cron: "0 7 * * 1"
      mode: "report-only"
mcp_tools:
  required:
    - "mcp__gsc__enhanced_search_analytics"
  optional:
    - "mcp__dataforseo__dataforseo_labs_google_historical_rank_overview"
budget:
  uses_paid_mcp: false
  estimated_credits: 0
autonomy:
  confidence: HIGH
  requires_approval: true
  safe_auto_execute: false
---

# content-decay — discovery skill (Phase 7 Wave 1)

10-step protocol. Steps map 1:1 to `workflow_runner` invocations + the
spec §16.5 8-step MCP discipline. Raw JSON drift recovery is mandatory:
every MCP response is dropped into `inbox/gsc/` (or `inbox/dfs/` for
the optional historical-rank cross-check) *before* any transform runs,
so a transform bug never costs us the upstream payload.

This skill follows the **convention authority** established by
`skills/discovery/quick-wins/SKILL.md` + the Phase 6 GSC ingestion
paterni `skills/ingestion/gsc-pull/SKILL.md`. The 10-step protocol shape,
raw JSON inbox discipline, URL normalization (D-03), DURUR + flag rule,
and provenance event format are reused verbatim — only the domain content
(window semantics, transform script, target sheet, optional DFS branch)
changes. Deviate only with an ADR.

## Inputs (frontmatter contract)

| Name             | Type    | Default | Notes                                                  |
|------------------|---------|---------|--------------------------------------------------------|
| `project_slug`   | string  | —       | Required. Resolves `projects/{slug}/master.xlsx`.       |
| `days_back`      | integer | 90      | Recent window end=today, start=today-N. Previous window same length. |
| `dimensions`     | array   | ['page']| GSC dimensions for enhanced_search_analytics calls.    |

`workspace_root` is resolved via `PSEO_WORKSPACE_ROOT` env or explicit
test override (mirrors workflow_runner / events_writer).

## Outputs (artifacts produced)

- `projects/{slug}/master.xlsx#content_decay` — per-page recent vs
  previous window decay rows (8 cols, schema-locked).
- `projects/{slug}/outputs/reports/{date}-content-decay.md` — human-
  readable summary (top decayed pages, trend distribution, pillars).
- `projects/{slug}/_state/events.jsonl` — `event_kind=provenance`
  entries (`source.kind=gsc_mcp`; one per MCP call, plus optionally one
  `dataforseo_mcp` entry when the optional rank cross-check fires).
- `projects/{slug}/inbox/gsc/{date}-enhanced_search_analytics-decay-recent-{slug}.json`
  — raw MCP payload for the recent 90-day window (drift recovery).
- `projects/{slug}/inbox/gsc/{date}-enhanced_search_analytics-decay-previous-{slug}.json`
  — raw MCP payload for the previous 90-day window.
- (optional) `projects/{slug}/inbox/dfs/{date}-historical_rank_overview-decay-{slug}.json`
  — raw DFS historical rank cross-check payload (only when the
  optional branch is invoked AND its budget pre-flight passes).

## 10-Step Body Protocol

> Each step name must match the `steps[*].name` passed to
> `workflow_runner.create_run`. Names are stable identifiers across runs.

### Step 1 — `create_run`

Open a workflow run shell. The state file lives at
`projects/{slug}/_state/workflows/{run_id}.json` (ADR-021, ADR-019).

```python
from scripts.state import workflow_runner
handle = workflow_runner.create_run(
    skill="content-decay",
    project_slug=project_slug,
    steps=[
        {"name": "fetch_recent"},
        {"name": "fetch_previous"},
        {"name": "transform"},
        {"name": "request_approval"},
        {"name": "write_excel"},
        {"name": "render_report"},
    ],
)
```

### Step 2 — `fetch_recent` (MCP §16.5 step 3 — raw inbox first)

Recent window (today - days_back .. today).

```python
workflow_runner.start_step(handle.run_id, 0,
                           project_slug=project_slug)
raw_recent = mcp__gsc__enhanced_search_analytics(
    siteUrl=project_config["gsc"]["site_url"],
    startDate=(today - days_back).isoformat(),
    endDate=today.isoformat(),
    dimensions=dimensions,    # default: ["page"]
)
inbox_recent = (
    workspace_root / "projects" / project_slug
    / "inbox" / "gsc"
    / f"{today.isoformat()}-enhanced_search_analytics-decay-recent-{project_slug}.json"
)
inbox_recent.parent.mkdir(parents=True, exist_ok=True)
inbox_recent.write_text(json.dumps(raw_recent, ensure_ascii=False, indent=2))
workflow_runner.finish_step(handle.run_id, 0,
                            project_slug=project_slug,
                            output_ref=str(inbox_recent))
```

### Step 3 — `fetch_previous` (previous window for delta — MUST succeed)

`mcp__gsc__enhanced_search_analytics` covering the PREVIOUS window of
equal length (today - 2*days_back .. today - days_back). Persist to
`inbox/gsc/{date}-enhanced_search_analytics-decay-previous-{slug}.json`.

**Both windows are required.** Unlike gsc-pull (where previous-window
failure is non-fatal first-run), decay analysis has no signal without
the comparison baseline. Failure here triggers DURUR #1 (do NOT proceed
with one-window data).

### Step 4 — `transform`

Pure compute via `scripts/discovery/content_decay_transform.py`:

```bash
python3 scripts/discovery/content_decay_transform.py \
    --recent   inbox/gsc/{date}-enhanced_search_analytics-decay-recent-{slug}.json \
    --previous inbox/gsc/{date}-enhanced_search_analytics-decay-previous-{slug}.json \
    --output-dir _state/transform/{run_id}/
```

Produces a JSON array (`content_decay`) shaped to the master-excel
schema. URL normalization (D-03) is applied here, not at fetch time, so
the raw inbox copy is byte-faithful to the MCP response. The transform
is idempotent: same inputs → byte-identical output. Sort: `clicks_delta`
ascending (most decayed first), then url asc for tie-break determinism.

### Step 4b — `optional_dfs_cross_check` (only when invoked)

When the optional `mcp__dataforseo__dataforseo_labs_google_historical_rank_overview`
branch is active, **first** run the budget pre-flight via
`scripts/budget/check_budget.py` against the project's 24h DFS credit
total.

- Budget pre-flight PASS → fetch DFS historical rank, persist raw to
  `inbox/dfs/{date}-historical_rank_overview-decay-{slug}.json`,
  enrich `action` with a short note (e.g. "rank also dropped in DFS")
  for rows where the DFS series confirms the decay direction.
- Budget pre-flight FAIL → **DURUR #7** (`StopAndAwaitApproval`). Do
  NOT silently fall back to GSC-only. The skill exits with
  `awaiting_approval`; the manager must explicitly authorize either
  raising the budget cap or skipping the cross-check (recorded as an
  ADR), then resume.

```python
from scripts.budget import check_budget
budget_envelope = check_budget.preflight(
    project_config_path=project_root / "project.config.json",
    events_path=project_root / "_state" / "events.jsonl",
    estimated_credits=1,    # one DFS historical_rank_overview call
)
if budget_envelope["exceeded"]:
    workflow_runner.request_approval(
        handle.run_id, project_slug=project_slug,
        approver="user",
        subject="DFS historical_rank cross-check budget aşıldı; cap yükseltilsin mi?",
        step_index=3,
    )
    raise check_budget.BudgetGateError("DFS budget pre-flight failed; awaiting_approval")
```

### Step 5 — `request_approval` (skill EXIT awaiting_approval)

```python
workflow_runner.request_approval(
    handle.run_id, project_slug=project_slug,
    approver="user",
    subject=f"Content decay {len(rows)} URL hesaplandı, master.xlsx#content_decay'e yazalım mı?",
    step_index=3,
)
# Skill exits here. The user replies in a fresh session; resume below.
```

### Step 6 — Resume (`approve` → continue)

```python
workflow_runner.approve(handle.run_id, project_slug=project_slug,
                        approver="user")
```

### Step 7 — `write_excel` (atomic, schema-validated)

Single idempotent `committer.commit` call for the content_decay sheet
(replace — re-running refreshes the snapshot, never duplicates rows; routes
through transaction.py: backup + lock + schema validation + provenance).
**Note:** the transform module itself does NOT import the committer
(`scripts.orchestration.committer`) — only the skill orchestrator layer does
(cross-module IMPORT discipline).

```python
from scripts.orchestration import committer
committer.commit(
    workbook_path=workspace_root/"projects"/project_slug/"master.xlsx",
    sheet="content_decay",
    rows=content_decay_rows,
    run_id=handle.run_id,
    project_slug=project_slug,
    writer="content-decay",
)
```

### Step 8 — `render_report`

`render_template.py templates/reports/content-decay.template.md data.json`
→ `outputs/reports/{date}-content-decay.md`. Variables (all 13 the template
references — render hard-fails on any missing): `$project_slug`, `$date`,
`$days_back`, `$run_id`, `$total_urls`, `$rows_written`, `$decay_count`,
`$retired_count`, `$top_decay_url`, `$top_decay_delta`, `$trend_distribution`,
`$pillar_summary`, `$report_summary`.

### Step 9 — Provenance event

One provenance entry per MCP call (two for GSC, plus one DFS when the
optional branch fires).

```python
from scripts.state import events_writer
events_writer.append_provenance(
    project_id=project_slug,
    source={"kind": "gsc_mcp", "mcp_server": "gsc",
            "mcp_tool": "gsc__enhanced_search_analytics",
            "response_bytes": len(json.dumps(raw_recent))},
    operation="project_excel",
    target_excel_sheet="content_decay",
    rows_written=len(content_decay_rows),
)
# Repeat for raw_previous (also gsc_mcp). When DFS branch ran:
# source.kind=dataforseo_mcp + cost.credits=1 + budget_key.
```

### Step 10 — `complete`

```python
workflow_runner.complete(handle.run_id, project_slug=project_slug, outputs={
    # F5: outputs.* must be STRING-TYPED.
    "content_decay_rows":      str(len(content_decay_rows)),
    "decay_count":             str(meta["trend_counts"]["DECAY"]),
    "retired_count":           str(meta["trend_counts"]["RETIRED"]),
    "report_path":             str(report_path),
    "raw_recent":              str(inbox_recent),
    "raw_previous":            str(inbox_previous),
})
```

## URL normalization (D-03 invariant)

Every URL passing through this skill is normalized via
`scripts.discovery.content_decay_transform.normalize_url`. The function
is **idempotent**: `normalize_url(normalize_url(u)) == normalize_url(u)`.
Rules: lowercase scheme+host, IDN→punycode, strip default ports, strip
trailing slash (root excluded), drop fragment, drop tracking params,
sort remaining query keys.

The transform self-checks idempotency on every emitted URL — a drift
between transform and downstream consumers is escalated as DURUR #3
rather than silently masked.

## Period delta semantics

```
recent   window:  [today - days_back, today]            (default 90d)
previous window:  [today - 2*days_back, today - days_back]
```

Both windows must have **equal length**. The transform aggregates clicks +
impressions + impression-weighted position per normalized URL across each
window, then labels the trend per **R-85 (single source —
`rules/content-update-discipline.md`)**. The clicks `delta_pct` is still the
emitted column; position/impressions feed the DECISION only (the 8-column
schema is unchanged).

```
clicks_delta = clicks_recent - clicks_previous
delta_pct    = (clicks_recent - clicks_previous) / clicks_previous * 100,
               rounded 2dp; clamped to ±100 when previous=0
               (mirrors gsc-pull's _delta_pct, Phase 6 paterni)

trend  (R-85 multi-signal — see rules/content-update-discipline.md §R-85):
   NEW      if previous clicks=0 and recent>0
   RETIRED  if recent clicks=0  and previous>0
   GROWTH   if clicks delta_pct ≥ +20
   DECAY    if  (clicks Δ% < clicks_threshold  AND position worsened > position_threshold)
            OR  (impressions Δ% < -40%          AND ranking trend negative)
   STABLE   otherwise  ← a clicks-only drop with NO position/impression
                          corroboration is STABLE, not DECAY (single-signal
                          volatility ≠ decay — R-85 rationale)

profile-aware thresholds (clicks/position branch; impressions branch fixed):
   YMYL        : clicks_threshold -20%, position_threshold +3  (stricter)
   e-commerce  : -30% / +5
   other/default: -30% / +5
   (resolved via scripts/util/profile_aware_defaults.cascade_default — a
    project.config tuning key or an explicit override wins over the profile
    default; meta records the resolved thresholds + which R-85 branch fired.)

pillar  = first non-empty path segment (e.g. /blog/x → "blog")
action  = trend → deterministic prescription
            DECAY   → "investigate + refresh"
            STABLE  → "monitor"
            GROWTH  → "double-down"
            NEW     → "new content tracking"
            RETIRED → "redirect or revive review"
```

**YoY mode (`--yoy`):** compares the recent window against the *same window
one year earlier* (the `--previous` payload is that year-ago window). When
the year-ago data is present it is used as the decay baseline; when it is
absent the transform refuses to fabricate verdicts — it emits a
`meta.yoy_unavailable=true` note and zero rows (never fakes a YoY baseline).

## DURUR conditions (9)

Stop and flag the manager — do not patch, do not fall back.

1. Either `mcp__gsc__enhanced_search_analytics` call (recent OR
   previous) returns auth/network/scope error. Decay analysis has no
   signal without both windows; STOP — do not proceed with one-window
   data.
2. Raw JSON inbox path cannot be created (workspace path missing or
   non-writable).
3. URL normalization output drifts (idempotency check fails inside
   `content_decay_transform.transform`).
4. `master.xlsx#content_decay` column count or names don't match
   schema (`schemas/master-excel.schema.json#content_decay`,
   8 required_columns).
5. `committer.commit` raises `RowSchemaError` for `content_decay`.
6. `workflow_runner.create_run` fails schema validation.
7. **Optional DFS `historical_rank_overview` invoked AND budget pre-
   flight FAILS** → STOP, transition to `awaiting_approval`. Manager
   bilgilendirilir; do NOT silently fall back to GSC-only (false-success
   failure mode the brief explicitly forbids).
8. Both windows aggregate to zero rows (`ContentDecayError: no data in
   window`). Decay-specific: there is no signal to project.
9. Window dates overlap (caller logic error; recent.start ≤
   previous.end). Decay-specific guard against silently double-counting.
   The skill orchestrator validates window arithmetic before fetch.

`PSEO_WORKSPACE_ROOT` env unset → also a STOP (inherited from
`workflow_runner` / `events_writer` discipline; folded into DURUR #2's
"workspace path missing" surface).

## Cross-references

- Schemas: `schemas/master-excel.schema.json` (content_decay sheet,
  8 required_columns at lines 173-180), `schemas/events.schema.json`
  (`source.kind=gsc_mcp` / `dataforseo_mcp`,
  `target_excel_sheet=content_decay`),
  `schemas/cross-sheet-invariants.json` (D-03 invariant),
  `schemas/skill-frontmatter.schema.json` (this frontmatter).
- Cross-modules (IMPORT-only): `scripts/state/workflow_runner.py`,
  `scripts/excel/transaction.py`, `scripts/state/events_writer.py`,
  `scripts/reporting/render_template.py`,
  `scripts/budget/check_budget.py` (optional DFS branch only —
  not for the GSC-only path).
- Transform: `scripts/discovery/content_decay_transform.py`
  (`normalize_url`, `infer_pillar`, `transform`,
  `CONTENT_DECAY_COLUMNS`, `ContentDecayError`).
- Tests: `tests/skills/test_content_decay.py` (≥6 cases incl. trend
  coverage, ±100 clamp, sort, schema column match, smoke E2E).
- Template: `templates/reports/content-decay.template.md`.

## Discipline checklist

- [x] TODO/fallback YASAK — every DURUR raises, none silently downgrade.
      The optional DFS branch's budget-fail path is `awaiting_approval`,
      not a silent GSC-only fallback.
- [x] Schema-first — frontmatter validates against
      `schemas/skill-frontmatter.schema.json` Draft 7.
- [x] Plugin-agnostik — no slug literals; `project_slug` flows through.
      0-tolerance ADR-008 + D-010 Path B.
- [x] ADR-013: `Use when`/`Also use when`/`Do not use when` are STRING
      content inside `description`, not separate fields.
- [x] Cross-module IMPORT discipline — `transaction` /
      `workflow_runner` / `events_writer` / `check_budget` are imported,
      never modified from this skill. The transform module itself does
      NOT import `scripts.excel.transaction` (skill-orchestrator-only).
- [x] F1: write target is `master.xlsx` (lowercase, schema-shaped).
- [x] F5: `outputs.*` values are STRING-TYPED (artifact paths or
      stringified counts), never raw ints.
- [x] Append-only state — events emitted via `events_writer.append_provenance`,
      one per MCP call; workflow run JSON only forward-flips step status.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.