Semantic Decay & Freshness Engine β Multi-Platform Monorepo
Detect stale dates, outdated version strings, deprecated tech references, broken links, and rotting content β before your readers do. Works across every major CMS.
Strapi Content Manager β articles monitored by the plugin:
Article with stale content β decay signals visible in the body:
Strapi Freshness Dashboard β real-time decay scores across all monitored collections:
CLI β analyze any file:
$ cpulse analyze getting-started-react.md
Score: 15/100 [Critical]
β [date_decay] Stale date: "January 2019" (7 year(s) ago)
β [version_decay] Version string "v2.1.0" β verify it's still current
β [stale_reference] Contains placeholder: "TBD"
β [tech_decay] Create React App is officially deprecated (2023)
β [tech_decay] Python 2 is EOL since January 2020
$ cpulse analyze typescript-5-best-practices.md
Score: 100/100 [Fresh]
β No decay detected
| Package | npm | Platform | Description |
|---|---|---|---|
@contentpulse/core |
npm i @contentpulse/core |
β | Shared analyzer engine (no platform deps) |
@contentpulse/payload |
npm i @contentpulse/payload |
Payload CMS v3 | Plugin with lifecycle hooks + admin fields |
@contentpulse/contentful |
npm i @contentpulse/contentful |
Contentful | Sidebar app + Entry Field SDK integration |
strapi-plugin-content-pulse |
npm i strapi-plugin-content-pulse |
Strapi v5 | Separate repo β webhook, cron, REST API, dashboard |
contentpulse/ (this repo)
βββ packages/
β βββ core/ β @contentpulse/core
β β βββ src/
β β βββ analyzer.ts chrono-node + version + stale-ref + tech-decay + custom rules
β β βββ link-checker.ts async broken-link detection (url_rot)
β β βββ types.ts PulseWarning, AnalyzerConfig, scoring helpers
β β βββ index.ts
β β
β βββ contentful/ β @contentpulse/contentful
β β βββ src/
β β βββ extractor.ts Contentful rich-text β plain text
β β βββ adapter.ts bridge to @contentpulse/core
β β βββ components/
β β β βββ PulseWidget.tsx
β β βββ index.tsx
β β
β βββ payload/ β @contentpulse/payload
β βββ src/
β βββ extractor.ts Lexical JSON β plain text
β βββ index.ts Plugin + afterChange hook
contentpulse-strapi/ (separate repo)
github.com/mturac/contentpulse-strapi
Strapi v5 β webhook, cron, dashboard, REST API, CSV/JSON export
| Warning Type | Examples | Notes |
|---|---|---|
date_decay |
"Updated January 2022", "as of last year" | NLP date parsing via chrono-node |
version_decay |
"v3.2.1", "2023 edition" | Ignores IP addresses (192.168.x.x) |
stale_reference |
"deprecated", "coming soon", "TBD", "formerly" | Deduplicated β 10Γ same word = 1 warning |
tech_decay |
Create React App, Python 2, IE11, Flash, AngularJS v1, Moment.js, TSLint | 15+ EOL technologies tracked |
url_rot |
https://old-site.com/404 |
Async HEAD check, timeout-aware |
| Severity | Deduction | Trigger |
|---|---|---|
| Critical | β25 pts | > 3 years old / EOL tech |
| High | β15 pts | > 2 years old |
| Medium | β10 pts | > 1.5 years old |
| Low | β5 pts | Generic version string |
Range: 0 (fully decayed) β 100 (fresh). Score never goes below 0.
Any adapter β or your own custom integration β can use @contentpulse/core directly:
import { analyzeTexts, getScoreColor, getScoreLabel } from '@contentpulse/core'
const result = analyzeTexts([
{ text: 'Updated January 2022. Requires Node.js v14.', field: 'body' },
{ text: 'Built with Create React App. Python 2 compatible.', field: 'intro' },
])
console.log(result.score) // e.g. 30
console.log(getScoreLabel(30)) // "Critical"
console.log(getScoreColor(30)) // "#ef4444"
console.log(result.warnings)
// [
// { type: 'date_decay', severity: 'critical', field: 'body', message: 'Stale date: "January 2022"β¦' },
// { type: 'tech_decay', severity: 'high', field: 'body', message: 'Node.js v14 is EOLβ¦' },
// { type: 'tech_decay', severity: 'high', field: 'intro', message: 'Create React App is deprecatedβ¦' },
// { type: 'tech_decay', severity: 'critical', field: 'intro', message: 'Python 2 is EOL since January 2020β¦' },
// ]Define your own patterns β domain changes, internal product renames, legacy phrases:
import { analyzeText } from '@contentpulse/core'
const result = analyzeText('Contact us at support@oldcompany.com', {
customRules: [
{
pattern: 'oldcompany\\.com',
message: 'Domain has changed β use newcompany.com',
severity: 'high',
},
{
pattern: 'v1\\.x',
message: 'v1.x is no longer supported',
severity: 'critical',
},
],
})For multilingual setups, pass a BCP-47 locale to skip English-only detectors:
// Turkish content: skips date_decay (chrono-node is English-only)
// and tech_decay (English tech names won't match Turkish text)
const result = analyzeText('Bu iΓ§erik Ocak 2019 tarihlidir.', { locale: 'tr' })
// version_decay still runs for all locales β "v2.0.1" is universalSupported: any non-en locale skips date_decay + tech_decay. version_decay, stale_reference, and customRules always run.
import { checkLinks } from '@contentpulse/core'
const results = await checkLinks(articleBody, {
timeout: 5000, // ms per request (default: 5000)
concurrency: 5, // parallel HEAD requests (default: 5)
})
// results[]: { url, status, ok, warning? }
// warning is a PulseWarning of type 'url_rot' when status >= 400 or request fails// payload.config.ts
import { contentPulse } from '@contentpulse/payload'
export default buildConfig({
plugins: [
contentPulse({
collections: ['posts', 'articles', 'docs'],
warningThreshold: 80,
maxAgeDays: 365,
}),
],
})Injects _pulseScore, _pulseWarnings, _lastAnalyzedAt into every monitored collection via afterChange hook.
Install the Contentful App via the marketplace or sideload in development. Displays a PulseWidget in the Entry sidebar showing:
- Circular score badge (0β100)
- Severity-filtered warning list
- Re-analyze button
- Per-warning type + suggestion
Config via App Parameters: decayThresholdDays (default: 365).
β github.com/mturac/contentpulse-strapi
Full-featured standalone plugin:
- Webhook / Slack Block Kit notifications on decay
- Daily cron bulk re-analysis with batch processing
- Admin Freshness Dashboard with filters, sort, per-row re-analyze
- REST API:
/api/content-pulse/dashboard,/reanalyze/:uid/:id - Export as CSV or JSON β full audit trail download from dashboard
# Install all workspaces
npm install
# Run all tests (51 tests, 0 failures)
npm test
# Build all packages
npm run build
# Work on a specific package
npm run test:core
npm run test:payload
npm run test:contentfulpackages/core β 51 tests passing
β analyzeText β clean content (3)
β date_decay β NLP parsing + severity ladder (4)
β version_decay β semver, IP exclusion, year editions (5)
β stale_reference β dedup, score floor (4)
β tech_decay β 8 EOL technologies + modern-tech exclusion (9)
β analyzeTexts β multi-field aggregation (3)
β metadata β analyzedAt ISO timestamp (1)
β calcScore / getScoreColor / getScoreLabel (8)
β custom rules β pattern match, dedup, severity (3)
β locale-aware β tr skips date/tech, runs version (4)
β extractUrls β http/https, dedup, punctuation strip (4)
-
url_rotβ broken link detection via async HEAD check - Custom decay rules β user-defined regex patterns with severity
- Locale-aware analysis β skip English-only detectors for non-
encontent - npm publish config β all packages publish-ready
- CSV/JSON export β audit trail download (Strapi plugin)
- Score history / trend tracking (rolling JSON window)
- Public freshness badge API
- Community-maintained tech decay dictionary
MIT Β© Mehmet Turac


