Skip to content

Remove previous and next post helper pagination counts#27431

Open
jonatansberg wants to merge 1 commit intomainfrom
onc-1653-performance-issues
Open

Remove previous and next post helper pagination counts#27431
jonatansberg wants to merge 1 commit intomainfrom
onc-1653-performance-issues

Conversation

@jonatansberg
Copy link
Copy Markdown
Member

@jonatansberg jonatansberg commented Apr 16, 2026

Summary

This changes the previous and next post helpers so their adjacent-post lookups do not calculate pagination totals. Those helpers only render a single neighboring post, so the extra count query was unnecessary and expensive on sites with large post tables and high traffic.

The no-pagination path is opt-in from the helper context, and the public posts cache key includes that mode so cached helper responses stay separate from normal paginated browse responses.

Testing

  • pnpm --dir ghost/core test:single test/unit/frontend/helpers/prev-post.test.js
  • pnpm --dir ghost/core test:single test/unit/frontend/helpers/next-post.test.js
  • pnpm --dir ghost/core test:single test/unit/server/models/post.test.js
  • pnpm --dir ghost/core test:single test/e2e-frontend/helpers/next-post.test.js
  • pnpm --dir ghost/core lint

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 16, 2026

Walkthrough

A skipPagination option was introduced and propagated from frontend helpers (prev_post/next_post) into the public posts browse endpoint and into the model findPage. When skipPagination: true, findPage applies a manual limit/offset window, sets options.limit to 'all', and returns empty meta ({}) instead of pagination data. Tests (unit and e2e) were added or updated to assert the option is passed and that pagination count queries are skipped.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and clearly describes the main change: removing pagination count queries from the previous and next post helpers.
Description check ✅ Passed The description is well-related to the changeset, explaining why pagination counts are being removed and how the opt-in mechanism and cache key updates work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch onc-1653-performance-issues

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jonatansberg jonatansberg changed the title Fix previous and next post helper pagination counts Remove previous and next post helper pagination counts Apr 16, 2026
@jonatansberg jonatansberg force-pushed the onc-1653-performance-issues branch from e4b08d5 to d4975a7 Compare April 16, 2026 15:32
@jonatansberg jonatansberg marked this pull request as ready for review April 16, 2026 15:32
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4975a78ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ghost/core/core/server/api/endpoints/posts-public.js Outdated
@jonatansberg jonatansberg force-pushed the onc-1653-performance-issues branch from d4975a7 to cf1e233 Compare April 16, 2026 15:43
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ghost/core/test/unit/frontend/helpers/next-post.test.js (1)

520-527: ⚠️ Potential issue | 🟡 Minor

Auth test assertion will fail due to exact match on context.

Line 525 uses context: {member} as an exact match, but the production code now sets context: { member, includePagination: false }. This test will fail because the actual context object includes the includePagination property.

Update to use sinon.match for a partial match, consistent with the "with valid post data" test:

🐛 Proposed fix
             sinon.assert.calledOnceWithExactly(
                 browsePostsStub,
                 sinon.match({
                     include: 'author,authors,tags,tiers',
                     // Check context passed
-                    context: {member}
+                    context: sinon.match({member, includePagination: false})
                 })
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/unit/frontend/helpers/next-post.test.js` around lines 520 -
527, The assertion uses an exact object match for the browsePostsStub call which
fails because production now sends context with extra properties; update the
expectation to allow a partial match by replacing the exact context object
(context: {member}) with a sinon.match that checks context contains member (e.g.
context: sinon.match({member})) so browsePostsStub is asserted with
sinon.match({ include: 'author,authors,tags,tiers', context:
sinon.match({member}) }); ensure you keep the existing include check and use
sinon.match for partial matching of context in the test around browsePostsStub.
🧹 Nitpick comments (1)
ghost/core/test/unit/frontend/helpers/prev-post.test.js (1)

512-519: Auth test should also verify includePagination: false for consistency.

The "should pass the member context" test at lines 512-519 only asserts context: sinon.match({member}), but doesn't verify that includePagination: false is also present. For consistency with the "with valid post data" test (lines 71-74), consider updating this assertion:

♻️ Suggested fix
             sinon.assert.calledOnceWithExactly(
                 browsePostsStub,
                 sinon.match({
                     include: 'author,authors,tags,tiers',
                     // Check context passed
-                    context: sinon.match({member})
+                    context: sinon.match({member, includePagination: false})
                 })
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/unit/frontend/helpers/prev-post.test.js` around lines 512 -
519, The test assertion for browsePostsStub should also assert
includePagination: false; update the sinon.assert.calledOnceWithExactly call to
match include: 'author,authors,tags,tiers' and context: sinon.match({member})
plus ensure the options object passed to browsePostsStub contains
includePagination: false (e.g., add includePagination: false inside the
sinon.match used for the second arg) so it mirrors the "with valid post data"
test's expectations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@ghost/core/test/unit/frontend/helpers/next-post.test.js`:
- Around line 520-527: The assertion uses an exact object match for the
browsePostsStub call which fails because production now sends context with extra
properties; update the expectation to allow a partial match by replacing the
exact context object (context: {member}) with a sinon.match that checks context
contains member (e.g. context: sinon.match({member})) so browsePostsStub is
asserted with sinon.match({ include: 'author,authors,tags,tiers', context:
sinon.match({member}) }); ensure you keep the existing include check and use
sinon.match for partial matching of context in the test around browsePostsStub.

---

Nitpick comments:
In `@ghost/core/test/unit/frontend/helpers/prev-post.test.js`:
- Around line 512-519: The test assertion for browsePostsStub should also assert
includePagination: false; update the sinon.assert.calledOnceWithExactly call to
match include: 'author,authors,tags,tiers' and context: sinon.match({member})
plus ensure the options object passed to browsePostsStub contains
includePagination: false (e.g., add includePagination: false inside the
sinon.match used for the second arg) so it mirrors the "with valid post data"
test's expectations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c12a6060-b854-4e61-bbed-8674f3b6a277

📥 Commits

Reviewing files that changed from the base of the PR and between 434af39 and d4975a7.

📒 Files selected for processing (6)
  • ghost/core/core/frontend/helpers/prev_post.js
  • ghost/core/core/server/api/endpoints/posts-public.js
  • ghost/core/core/server/models/base/plugins/crud.js
  • ghost/core/test/unit/frontend/helpers/next-post.test.js
  • ghost/core/test/unit/frontend/helpers/prev-post.test.js
  • ghost/core/test/unit/server/models/post.test.js

@jonatansberg jonatansberg force-pushed the onc-1653-performance-issues branch from cf1e233 to 670be2a Compare April 16, 2026 15:48
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
ghost/core/test/e2e-api/content/posts.test.js (1)

454-456: Make SQL detection less quote/case sensitive.

The current string includes can miss equivalent SQL forms. A regex-based check is more robust across SQL formatting differences.

Proposed tweak
-        const postsCountQueries = queries.filter((query) => {
-            return query.sql.includes('count(') && query.sql.includes('`posts`');
-        });
+        const postsCountQueries = queries.filter((query) => {
+            return /\bcount\s*\(/i.test(query.sql) && /\bfrom\s+[`"]?posts[`"]?/i.test(query.sql);
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/e2e-api/content/posts.test.js` around lines 454 - 456, The
SQL detection using query.sql.includes('count(') &&
query.sql.includes('`posts`') is too brittle; replace the string includes in the
postsCountQueries filter with a case-insensitive regex test on query.sql (e.g.,
match \bcount\s*\( and \bposts\b allowing optional backticks) so different
quoting/casing/spacing still matches; update the filter that constructs
postsCountQueries to use regex.test(query.sql) (or two regex tests) instead of
the current includes checks to make detection robust.
ghost/core/core/server/models/base/plugins/crud.js (1)

177-182: Consider documenting the meta shape change inline.

When includePagination is false, meta is intentionally empty. A short inline note here (or updated JSDoc) would reduce accidental meta.pagination assumptions by future callers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/core/server/models/base/plugins/crud.js` around lines 177 - 182,
Add a short inline comment (or update the surrounding JSDoc) where the early
return occurs to explicitly state that when includePagination is false the
returned object will contain data and an intentionally empty meta (no
meta.pagination), so callers should not assume meta.pagination exists; reference
the includePagination check and the returned meta object in your note so future
readers seeing the return block (data: data, meta: {}) understand the shape
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ghost/core/core/server/models/base/plugins/crud.js`:
- Around line 153-155: Page and limit values from options can be negative
producing negative offsets; clamp parsed values to a minimum of 1 before
computing the offset. After parsing options.limit and options.page into limit
and page in the block guarded by includePagination (variables:
includePagination, options.limit, options.page, limit, page), enforce limit =
Math.max(1, limit) and page = Math.max(1, page) (or equivalent) so offset
calculation later uses non-negative inputs and then proceed to compute offset as
before.

---

Nitpick comments:
In `@ghost/core/core/server/models/base/plugins/crud.js`:
- Around line 177-182: Add a short inline comment (or update the surrounding
JSDoc) where the early return occurs to explicitly state that when
includePagination is false the returned object will contain data and an
intentionally empty meta (no meta.pagination), so callers should not assume
meta.pagination exists; reference the includePagination check and the returned
meta object in your note so future readers seeing the return block (data: data,
meta: {}) understand the shape change.

In `@ghost/core/test/e2e-api/content/posts.test.js`:
- Around line 454-456: The SQL detection using query.sql.includes('count(') &&
query.sql.includes('`posts`') is too brittle; replace the string includes in the
postsCountQueries filter with a case-insensitive regex test on query.sql (e.g.,
match \bcount\s*\( and \bposts\b allowing optional backticks) so different
quoting/casing/spacing still matches; update the filter that constructs
postsCountQueries to use regex.test(query.sql) (or two regex tests) instead of
the current includes checks to make detection robust.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 92e121ab-c1cd-454d-9499-2721f78f9e8d

📥 Commits

Reviewing files that changed from the base of the PR and between d4975a7 and cf1e233.

📒 Files selected for processing (7)
  • ghost/core/core/frontend/helpers/prev_post.js
  • ghost/core/core/server/api/endpoints/posts-public.js
  • ghost/core/core/server/models/base/plugins/crud.js
  • ghost/core/test/e2e-api/content/posts.test.js
  • ghost/core/test/unit/frontend/helpers/next-post.test.js
  • ghost/core/test/unit/frontend/helpers/prev-post.test.js
  • ghost/core/test/unit/server/models/post.test.js
✅ Files skipped from review due to trivial changes (2)
  • ghost/core/test/unit/frontend/helpers/prev-post.test.js
  • ghost/core/test/unit/server/models/post.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • ghost/core/test/unit/frontend/helpers/next-post.test.js
  • ghost/core/core/frontend/helpers/prev_post.js
  • ghost/core/core/server/api/endpoints/posts-public.js

Comment thread ghost/core/core/server/models/base/plugins/crud.js Outdated
@jonatansberg jonatansberg force-pushed the onc-1653-performance-issues branch 3 times, most recently from 94cab6c to 724ca98 Compare April 16, 2026 15:55
ref https://linear.app/ghost/issue/ONC-1653/performance-issues-on-id1223916

The prev_post and next_post helpers only need one adjacent post, but the public browse path also calculated full pagination metadata. Skipping that count for these helper lookups avoids expensive post table scans on large, high-traffic sites while preserving normal pagination for other callers.
@jonatansberg jonatansberg force-pushed the onc-1653-performance-issues branch from 724ca98 to 62b7412 Compare April 16, 2026 15:57
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
ghost/core/core/server/models/base/plugins/crud.js (1)

22-22: Optional: prefer Number.parseInt for consistency with modern JS style rules.

Non-blocking, but this will satisfy the static-analysis preference and keep parsing style explicit.

Suggested diff
-    const parsedValue = parseInt(value, 10);
+    const parsedValue = Number.parseInt(value, 10);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/core/server/models/base/plugins/crud.js` at line 22, The code uses
the global parseInt to convert value into an integer (const parsedValue =
parseInt(value, 10)); replace that call with Number.parseInt(value, 10) to align
with modern JS style and static-analysis preferences; update the expression
where parsedValue is assigned so it uses Number.parseInt while leaving variable
and surrounding logic (parsedValue, value) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ghost/core/core/server/models/base/plugins/crud.js`:
- Line 22: The code uses the global parseInt to convert value into an integer
(const parsedValue = parseInt(value, 10)); replace that call with
Number.parseInt(value, 10) to align with modern JS style and static-analysis
preferences; update the expression where parsedValue is assigned so it uses
Number.parseInt while leaving variable and surrounding logic (parsedValue,
value) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 333b012d-68ff-47db-8e5d-b0b2936b3b7d

📥 Commits

Reviewing files that changed from the base of the PR and between cf1e233 and 62b7412.

📒 Files selected for processing (8)
  • ghost/core/core/frontend/helpers/prev_post.js
  • ghost/core/core/server/api/endpoints/posts-public.js
  • ghost/core/core/server/models/base/plugins/crud.js
  • ghost/core/core/server/models/base/plugins/sanitize.js
  • ghost/core/test/e2e-api/content/posts.test.js
  • ghost/core/test/unit/frontend/helpers/next-post.test.js
  • ghost/core/test/unit/frontend/helpers/prev-post.test.js
  • ghost/core/test/unit/server/models/post.test.js
✅ Files skipped from review due to trivial changes (1)
  • ghost/core/core/frontend/helpers/prev_post.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • ghost/core/core/server/api/endpoints/posts-public.js
  • ghost/core/test/unit/frontend/helpers/prev-post.test.js
  • ghost/core/test/unit/server/models/post.test.js

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant