Skip to content

fix(core): exclude unpublished entries from taxonomy counts#593

Open
maikunari wants to merge 1 commit intoemdash-cms:mainfrom
maikunari:fix/category-counts-unpublished
Open

fix(core): exclude unpublished entries from taxonomy counts#593
maikunari wants to merge 1 commit intoemdash-cms:mainfrom
maikunari:fix/category-counts-unpublished

Conversation

@maikunari
Copy link
Copy Markdown
Contributor

@maikunari maikunari commented Apr 16, 2026

Issue #581: Unpublished posts were included in category/tag counts, causing inaccurate public-facing sidebar counts.

Changes:

  • countEntriesWithTerm now defaults to status='published'
  • Added optional status parameter: 'all', 'published', 'draft', etc.
  • Uses raw SQL with sql.ref() and validateIdentifier for dynamic ec_{collection} table joins (Kysely type system limitation)
  • Updated existing test to reflect new default behavior
  • Added 4 new test cases covering all status filtering scenarios

What does this PR do?

Fixes #581 — unpublished posts were being counted in taxonomy term totals, inflating the public-facing category and tag counts shown in sidebars and archive pages.
Problem
TaxonomyRepository.countEntriesWithTerm counted all content_taxonomies rows matching a given taxonomy term, regardless of whether the linked entries were published. Draft, scheduled, and other non-published entries showed up in public counts, so a category labeled "12 posts" might only have 5 actually published.
Change

countEntriesWithTerm now defaults to status: 'published'
Added an optional status parameter accepting 'all', 'published', 'draft', etc.
Status-aware counting implemented via raw SQL joins against dynamic ec_{collection} content tables, with validateIdentifier for safety (Kysely's type system can't handle the dynamic table names)
Updated the existing test to reflect new default behavior
Added 4 new test cases covering the status filtering scenarios

Scope
Intentionally narrow. Default behavior change is the fix; the status parameter is additive for callers that need draft counts (admin UI, etc.).

Closes #581

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation and pnpm locale:extract has been run (if applicable)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code

Screenshots / test output

Issue emdash-cms#581: Unpublished posts were included in category/tag counts,
causing inaccurate public-facing sidebar counts.

Changes:
- countEntriesWithTerm now defaults to status='published'
- Added optional status parameter: 'all', 'published', 'draft', etc.
- Uses raw SQL with sql.ref() and validateIdentifier for dynamic
  ec_{collection} table joins (Kysely type system limitation)
- Updated existing test to reflect new default behavior
- Added 4 new test cases covering all status filtering scenarios
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 16, 2026

⚠️ No Changeset found

Latest commit: 502874f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes incorrect public taxonomy term counts by excluding unpublished entries from counts by default, with an option to count entries across any status.

Changes:

  • Updated TaxonomyRepository.countEntriesWithTerm to default to counting only published entries and to support an 'all' mode or a specific status filter.
  • Implemented status-aware counting via raw SQL joins against dynamic ec_* content tables with identifier validation.
  • Updated and expanded unit tests to cover default behavior and status-filtering scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/core/src/database/repositories/taxonomy.ts Changes taxonomy term counting to default to published-only and adds status filtering with dynamic ec_* joins.
packages/core/tests/unit/taxonomies/taxonomies.test.ts Updates existing test expectation and adds new test cases for status filtering behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +308 to +309
WHERE ct.taxonomy_id = ${taxonomyId}
AND ${sql.ref(tableName)}.status = ${effectiveStatus}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status-filtered COUNT query doesn't exclude soft-deleted entries. Since ContentRepository.delete() only sets deleted_at (and this repo doesn't appear to clear content_taxonomies on delete), deleted-but-still-tagged entries will still be counted as long as their status matches. Add a ${sql.ref(tableName)}.deleted_at IS NULL predicate (and consider also adding ct.collection = ${collection} inside the loop to avoid scanning all content_taxonomies rows for the taxonomy on every per-collection count).

Suggested change
WHERE ct.taxonomy_id = ${taxonomyId}
AND ${sql.ref(tableName)}.status = ${effectiveStatus}
WHERE ct.taxonomy_id = ${taxonomyId}
AND ct.collection = ${collection}
AND ${sql.ref(tableName)}.status = ${effectiveStatus}
AND ${sql.ref(tableName)}.deleted_at IS NULL

Copilot uses AI. Check for mistakes.
Comment on lines +288 to +305
// For status filtering, we need raw SQL to join dynamic ec_* tables.
// Get distinct collections for this taxonomy term first.
const collections = await this.db
.selectFrom("content_taxonomies")
.select((eb) => eb.fn.count("entry_id").as("count"))
.select("collection")
.distinct()
.where("taxonomy_id", "=", taxonomyId)
.executeTakeFirst();
.execute();

if (collections.length === 0) return 0;

// Count entries matching status by joining each collection's content table
let totalCount = 0;
for (const { collection } of collections) {
const tableName = `ec_${collection}`;
validateIdentifier(tableName, "content table");
const result = await sql<{ count: number }>`
SELECT COUNT(ct.entry_id) AS count
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation adds 1 query to fetch collections plus 1 COUNT query per collection, per taxonomy term. In handleTermList (api/handlers/taxonomies.ts) this method is called inside a loop over all terms, which can turn into hundreds/thousands of DB round-trips and be a noticeable regression. Consider building a single UNION ALL query across the relevant ec_* tables (similar to fetchRecentItems in api/handlers/dashboard.ts) or adding a bulk repo method that returns counts for many taxonomy IDs in one call so the handler can fetch all counts at once.

Copilot uses AI. Check for mistakes.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 16, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@593

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@593

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@593

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@593

emdash

npm i https://pkg.pr.new/emdash@593

create-emdash

npm i https://pkg.pr.new/create-emdash@593

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@593

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@593

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@593

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@593

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@593

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@593

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@593

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@593

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@593

commit: 502874f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unpublished Posts are included in Category counts

2 participants