Version 1.5.0 adds automatic duplicate post detection across all API webhooks, a built-in duplicate finder tool in the admin panel, and intelligent featured image thumbnail fallback using the image proxy. Applied to all 14 installations simultaneously.
Duplicate Post Prevention (API)
Every content-creating webhook now checks for existing posts with the same title or slug before inserting a new record. The Post::checkDuplicate() method queries against both fields, excluding trashed posts. When a duplicate is detected, the API returns HTTP 409 with the existing post's details — ID, title, slug, status, and URL — so the calling system can decide how to proceed.
Five webhook endpoints are protected: webhookPublish, webhookSchedule, webhookDraft, webhookContentGenerate, and webhookBulkPublish. The first four return a 409 error; bulk publish silently skips duplicates and reports a skipped count in the response. All endpoints respect force_duplicate: true in the request body to bypass the check when intentional duplicates are needed.
// API 409 Response
{
"success": false,
"error": "duplicate_post",
"existing": {
"id": 142,
"title": "Post Title",
"slug": "post-title",
"status": "published",
"url": "https://site.com/post-title"
}
}
Duplicate Finder (Admin Panel)
A new "Duplicates" button appears on the Posts page. Clicking it scans all published posts for slug patterns ending with -N (where N is 1 through 10) and checks whether the base slug also exists as a separate post. Results appear in a modal with side-by-side comparison of the duplicate and original, linking to each post's editor. Individual posts can be trashed one by one, or all detected duplicates can be removed in a single action.
Featured Image Fallback Without a Size Variant
The get_featured_image() function handles a missing sized variant gracefully, but not through a resize-on-demand proxy. If a specific size (thumbnail, small, medium, large, full, pinterest, or og) wasn't generated on disk for a given image, the function falls back through the format cascade instead: it tries the AVIF version, then WebP, then finally the original upload, serving whichever of those actually exists rather than a dynamically resized copy or a 404.
jekcms generates seven named size variants at upload time: thumbnail (150×150), small (300×200), medium (600×400), large (1200×800), full (1920×1280), pinterest (1000×1500), and og (1200×630).
Deployment Scope
All changes were applied simultaneously to 14 targets: 11 active sites, the _template directory, and both the root and native JekCMS installations. Each target received 5 file updates — no database migration required.
How checkDuplicate() Works Internally
The method runs two SQL queries in sequence. The first checks for an exact slug match using WHERE slug = ? AND status != 'trashed'. If no match is found, a second query performs a case-insensitive title comparison with WHERE LOWER(title) = LOWER(?) AND status != 'trashed'. This two-step approach catches both exact reposts and content that was republished with a slightly different slug. Both queries run against indexed columns, so the check adds negligible overhead to the publish workflow while preventing the far more expensive problem of manually identifying and cleaning up duplicate content after the fact.
// Post.php - checkDuplicate method
public function checkDuplicate(string $title, string $slug): ?array
{
// Check slug first (faster, indexed)
$existing = $this->db->fetch(
"SELECT id, title, slug, status FROM posts
WHERE slug = ? AND status != 'trashed' LIMIT 1",
[$slug]
);
if ($existing) return $existing;
// Then check title (case-insensitive)
return $this->db->fetch(
"SELECT id, title, slug, status FROM posts
WHERE LOWER(title) = LOWER(?) AND status != 'trashed' LIMIT 1",
[$title]
);
}
Duplicate Finder: Detection Algorithm
The admin-side duplicate finder uses a different approach than the API check. Instead of comparing against incoming content, it scans the entire posts table for slug patterns that suggest accidental duplication. The algorithm identifies slugs ending with -1 through -10 and looks up the base slug (without the numeric suffix) in the same table.
What Gets Flagged
- Posts where both
example-postandexample-post-1exist - Multiple numbered variants:
example-post-1,example-post-2, etc. - Only posts with status
publishedordraftare checked; trashed posts are excluded - The scanner runs as an AJAX request against indexed columns, so it stays fast even on larger sites
Simultaneous Deployment Across 14 Targets
Deploying to 14 targets required a structured approach. Each target received the same 5 files:
classes/Post.php— checkDuplicate() method addedapi/v1/index.php— 409 checks added to 5 webhook handlersadmin/ajax/find-duplicates.php— new file for duplicate scanningadmin/posts.php— UI button, modal, and JavaScript for duplicate finderincludes/helpers.php— get_featured_image() fallback logic
The deployment was file-only — no SQL migrations, no configuration changes, no service restarts. This made rollback straightforward: replacing any of the five files with its previous version would revert that specific change without affecting the others.