Skip to content

feat: replace blog pagination with "Load More" button and optional auto-scroll, improve noscript support#557

Open
stasadev wants to merge 5 commits intoddev:mainfrom
stasadev:20260224_stasasdev_infinite_scroll
Open

feat: replace blog pagination with "Load More" button and optional auto-scroll, improve noscript support#557
stasadev wants to merge 5 commits intoddev:mainfrom
stasadev:20260224_stasasdev_infinite_scroll

Conversation

@stasadev
Copy link
Member

@stasadev stasadev commented Feb 24, 2026

The Issue

Blog post listings used prev/next pagination links requiring full page navigations to browse content. Search results were capped at 10 with no way to see more, and did not restore when navigating back. Several site features (terminal animation, platform picker, code copy buttons) had no meaningful fallback without JavaScript.

How This PR Solves The Issue

"Load More" button and optional auto-scroll for blog listings

Replaces prev/next pagination with a fetch-HTML-fragment approach. Each listing page (index, category, author) renders its first 15 posts normally. A data-next-url attribute on the grid container points to the next pre-built static page. Clicking "Load More" fetches that URL, extracts the post cards via DOMParser, and appends them to the current grid. The next data-next-url is then read from the fetched page to continue the chain.

An "Auto-load on scroll" checkbox lets users switch to infinite scroll mode. The preference is saved to localStorage and applied on every blog listing page. It defaults to "Load More" button mode, keeping the footer accessible. The toggle is visible on all listing pages (including short ones) so the preference can always be changed.

This keeps every page's initial payload at 15 posts regardless of total post count, preserves full Astro image optimization on all cards (no client-side re-rendering), and leaves all paginated static routes intact as valid URLs and noscript fallbacks.

All logic is centralised in a new BlogPostGrid component. Paging.astro is removed as it is no longer referenced anywhere.

Progressive search results

Search results now load in batches of 10 as the user scrolls, with all matches held in memory after each query. Previously results were hard-capped at 10. The input value is also checked on page initialisation so results are restored correctly after browser back navigation.

Noscript fallbacks

  • BlogPostGrid: prev/next pagination links are rendered inside <noscript> tags; the "Load More" button is hidden by default and only revealed by JS
  • AnimatedTerminal: a static ddev describe output is shown via a second <pre> element; toggled by html[data-theme] (set by an inline head script before body renders) so there is no flash when JS is enabled
  • get-started.astro PlatformPicker: CSS :has() rules make radio-based platform selection work without JS
  • Code copy buttons: hidden via html:not([data-theme]) selector when JS is absent

Table wrapping

A new rehype plugin (rehype-wrap-tables.mjs) wraps markdown tables in a div.table-wrapper for horizontal scroll on mobile.

Manual Testing Instructions

Automated Testing Overview

No automated tests added; all changes are client-side JS and build-time HTML transforms.

Release/Deployment Notes

All paginated static routes (/blog/2/, /blog/category/x/2/, etc.) remain valid URLs. No redirects or deployment config changes needed.

🤖 Developed with assistance from Claude Code

## The Issue

Blog post listings used prev/next pagination links requiring full page
navigations to browse content. Tables in blog posts also lacked full-width
rendering and horizontal scroll on mobile. Search results were capped at 10
with no way to see more, and did not restore when navigating back.

## How This PR Solves The Issue

**Infinite scroll for blog listings**

Replaces pagination with a fetch-HTML-fragment approach. Each listing page
(index, category, author) renders its first 12 posts normally. A
`data-next-url` attribute on the grid container points to the next pre-built
static page. An `IntersectionObserver` fires when the user scrolls near the
bottom, fetches that URL, extracts the post cards via `DOMParser`, and appends
them to the current grid. The next `data-next-url` is then read from the
fetched page to continue the chain.

This keeps every page's initial payload at 12 posts regardless of total post
count, preserves full Astro image optimization on all cards (no client-side
re-rendering), and leaves all paginated static routes intact as valid URLs and
noscript fallbacks.

All logic is centralised in a new `BlogPostGrid` component. `Paging.astro` is
removed as it is no longer referenced anywhere.

**Infinite scroll for search**

Search results now load progressively in batches of 10 as the user scrolls,
with all matches held in memory after each query. Previously results were
hard-capped at 10. The input value is also checked on page initialisation so
results are restored correctly after browser back navigation.

**Full-width tables with mobile scroll**

`overflow-x: auto` on a `<table>` element has no effect because tables are
not block containers. The previous workaround (`display: block`) broke the
table's natural full-width behaviour. A new rehype plugin
(`rehype-wrap-tables.mjs`) wraps each `<table>` in a `<div class="table-wrapper">`
at build time, keeping the table as `display: table` (full width via Tailwind
Typography) while the block-level wrapper provides the scroll container.

## Manual Testing Instructions

- Visit /blog/ and scroll down to verify posts load without page navigation
- Visit /blog/category/<category>/ and /blog/author/<author>/ and scroll
- Check Network tab: each scroll fetch returns 200, not 301
- Disable JS and confirm the noscript fallback link is visible
- Search for a term with many results and scroll to load beyond the first 10
- Navigate to a result, press back, confirm results are restored
- Open a blog post with a wide table and verify it is full width on desktop
  and scrolls horizontally on mobile

## Automated Testing Overview

No automated tests added; all changes are client-side JS and build-time HTML
transforms.

## Release/Deployment Notes

All paginated static routes (/blog/2/, /blog/category/x/2/, etc.) remain
valid URLs. No redirects or deployment config changes needed.

🤖 Developed with assistance from [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Feb 24, 2026

🌐 Fork Preview for PR #557

https://pr-557.ddev-com-fork-previews.pages.dev

This preview updates automatically when you push changes to your fork.

Copy link
Member

@rfay rfay left a comment

Choose a reason for hiding this comment

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

That's amazing!

@tyler36
Copy link
Contributor

tyler36 commented Feb 25, 2026

Works well. Thank you.

The only "issue "with infinity scroll is you lose access to the page footer as you have to scroll increasing further to reach it. With this is mind, are there any links that would be better in the header?

@stasadev
Copy link
Member Author

stasadev commented Feb 25, 2026

The only "issue "with infinity scroll is you lose access to the page footer as you have to scroll increasing further to reach it. With this is mind, are there any links that would be better in the header?

It might be better to use a "Load More" button that fetches and appends items instead of loading them automatically.

Edit: I added a "Load More" button.

## Blog listing
- Replace infinite scroll with a "Load More" button, fixing the
  inaccessible footer problem
- Button is hidden by default, shown only when JS is available
- Button matches the site's hollow button style with dark mode support

## AnimatedTerminal
- Add noscript fallback: pre-render ddev describe output at build time
- Use CSS (data-theme attribute set by inline head script) to toggle
  between animated and static pre elements with zero flash
- Deduplicate describeOutput — defined once in frontmatter, passed via
  data attribute, read by the animation script

## get-started page
- Replace noscript message with CSS :has() rules so platform picker
  works without JS (clicking labels shows the correct platform section)

## global.css
- Hide copy buttons for noscript users (html:not([data-theme]))
- Add noscript notice for unrendered Mermaid diagrams
- Remove noscript dark mode CSS (not supported without JS)

🤖 Developed with assistance from [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@stasadev stasadev changed the title feat: replace blog pagination with infinite scroll feat: replace blog pagination with "Load More" and improve noscript support Feb 25, 2026
@stasadev stasadev requested a review from rfay February 25, 2026 11:38
@stasadev
Copy link
Member Author

stasadev commented Feb 25, 2026

Actually, I can add support for both "Load More" and infinite scroll, with "Load More" as the default.

Edit: done.

Adds a persistent "Auto-load on scroll" checkbox to all blog listing pages.
Preference is saved to localStorage so it applies across visits. Defaults
to "Load More" button mode. Toggle is always visible regardless of whether
a next page exists, allowing preference to be set on short category pages.

🤖 Developed with assistance from [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@stasadev stasadev changed the title feat: replace blog pagination with "Load More" and improve noscript support feat: replace blog pagination with "Load More" button and optional auto-scroll, improve noscript support Feb 25, 2026
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.

3 participants