Skip to content

feat: filter the list of posts by post type and text - #1016

Open
rmdes wants to merge 9 commits into
mainfrom
feat/posts-filter-by-post-type
Open

rmdes wants to merge 9 commits into
mainfrom
feat/posts-filter-by-post-type

Conversation

@rmdes

@rmdes rmdes commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #843.

The list of posts can't be narrowed down at all. Micropub q=source already supports filter and category (#991), but the posts interface doesn't use them, and there's no way to filter by post type.

Micropub

  • q=source accepts a post-type parameter, from the Query for Post List Micropub extension. Like filter and category, its value is cast to a string before it reaches the database. It combines with the other filters and with after/before paging.
  • A response that lists posts carries a private _count again: the number of posts matching the query, which may exceed the number of items on the page. This is the property added in f42a986 and dropped by 9bc6a35 when paging became cursor-based; the consumers added in cb541d4 stayed, so endpoint-posts has read undefined ever since, and endpoint-files still does. The count applies the same string-uid guard getCursor applies, so it never reports posts that paging cannot reach.
  • README updated.

Posts

  • A row of links above the list filters by post type, one per configured post type plus one for all of them. Choosing a post type is a single click, and the current filter is visible without opening anything. They use the existing navigation component with a navigation--filters modifier, which already carries the markup and the a[aria-current="true"] styling, so this adds a modifier rather than a component. The pill colours are the --color-offset / --color-on-offset pair the tag component uses, and the current link swaps them, so contrast is the same in both colour schemes.
  • The text search sits inside a details, collapsed unless a search term is in use.
  • Values are passed to the Micropub endpoint as post-type and filter, so the list stays a Micropub client and doesn't care how posts are stored. The post type links keep any search term and drop after/before, because a cursor names an item in the result set being left behind. The next and previous links keep every filter. Both are built with URLSearchParams, so search text such as lunch & dinner survives paging.

What the list says it is showing

A heading above the list says what is shown, and the same text is played back in the document title, which templates now take as an optional documentTitle local so it can differ from the page heading.

The rule is that the heading states what the controls do not already show. A post type is marked on its own link, so it appears as a prefix rather than inside the sentence; a search term's control is collapsed, so it is played back. A filter added later follows the same rule without needing new sentences, which I hope answers the concern about this not scaling.

613 posts
Note · 613 posts
Note · 613 posts matching “lunch”
Note · No posts
No posts matching “lunch”

The post type is a prefix for a second reason: post type names are not translatable. Every post type plugin hardcodes an English singular name and none ships a locale file, so "613 Note posts" renders in German as "613 Note Beiträge". As a standalone label it reads as a label in any language.

That needs three new keys rather than the twelve a sentence per filter combination would need, reusing the two empty-state keys already here.

Localisation

en.json only. The other locale files are unchanged, so the three new keys fall back to their English values until they are translated, which means a non-English admin UI shows English counts next to translated controls. noneMatching keeps its existing translations, which do not interpolate the search term, so those read as the unqualified sentence: true, just less specific than the English.

A key that exists only in en.json was rendered as the key itself rather than as English, because defaultLocale covers an unknown locale and not a missing key inside a known one. retryInDefaultLocale fixes that. Every locale file is complete on main, so this changes nothing visible today; it makes the window between a new key and a translation harmless. There is a test for it.

Left out

  • (showing posts 1 to 40). Paging is cursor-based, so a page does not know its own offset; working it out means a second count query on every page load for something pagination already covers. Happy to add it if you want it.
  • Counts on the post type links. These count the collection rather than the query, so they are a different thing from _count and no Micropub extension defines a surface for them.
  • Status and sort controls. q=source has no parameter for either.
  • limit is not carried into the next and previous links, so paging from ?limit=1 falls back to the default page size. main builds those hrefs as ?after=<uid> with nothing else, so this is not from this branch and I have left it alone.

Tests

  • Micropub: filtering by post type alone and combined with filter; _count matching each filter, exceeding the page, and excluding documents paging cannot reach.
  • Posts: the filtered list; exactly one current post type link; the links keeping the search term and dropping the cursor; the search form keeping the post type; the details collapsed unless searching; filters kept in the pagination links; the heading and document title in each state; and a search term containing & or <script> escaped exactly once in both the heading and the title.
  • i18n: a key present only in en.json falling back to English in a locale that has no translation for it, with a second case asserting that locale's own translations still win.
  • endpoint-micropub, endpoint-posts and frontend pass; lint, typecheck and prettier are clean.

@rmdes
rmdes marked this pull request as ready for review October 13, 2026 12:59
@paulrobertlloyd
paulrobertlloyd force-pushed the main branch 2 times, most recently from 2f22f32 to b791821 Compare October 13, 2026 18:24
@paulrobertlloyd
paulrobertlloyd force-pushed the feat/posts-filter-by-post-type branch from 1a2a11d to d04ab5c Compare October 13, 2026 18:27
@paulrobertlloyd

Copy link
Copy Markdown
Collaborator

Nice! Couple of things…

  1. Let’s put the filters inside a details component so that they are hidden/collapsed by default. That way they don’t take up too much screen space when not in use. The open attribute should be added if a post type or search filter is being used.
  2. Thinking about the accessibility of showing a subset of filtered items:
    • There should be a h2 above the items indicating what’s being shown
    • The page title should be updated to play back this same information
  3. Adding localisations is a bit more involved than updating the JSON files (at least for them to be managed via Localazy). New keys are added thus:
    1. Add/update en.json
    2. localazy upload (adds any new keys to locale dictionary)
    3. Add localisations in the Localazy UI
    4. localazy download (downloads updated localisation files)

I can add you as a contributor to Localazy if you’d like? Otherwise I can do the above once we’ve agreed on the localisations needed and their respective keys.

Playing back search results

Here’s an initial suggestion for the heading/page title. I realise this is possibly a bit much (would also need to think how this would need to adapt should further filters be added). Open to suggestions!

No filters applied

  • No posts
  • 1 post
  • 2 posts
  • 41 posts (showing posts 1 to 40)

Post type filter applied

  • No [TYPE] posts
  • 1 [TYPE] post
  • 2 [TYPE] posts
  • 41 [TYPE] posts (showing posts 1 to 40)

Search filter applied

  • No posts found matching ‘[QUERY]’
  • 1 post found matching ‘[QUERY]’
  • 2 posts found matching ‘[QUERY]’
  • 41 posts found matching ‘[QUERY]’ (showing posts 1 to 40)

Post type and search filter applied

  • No [TYPE] posts found matching ‘[QUERY]’
  • 1 [TYPE] post found matching ‘[QUERY]’
  • 2 [TYPE] posts found matching ‘[QUERY]’
  • 41 [TYPE] posts found matching ‘[QUERY]’ (showing posts 1 to 40)

The page title should use this same text, prefixed with ‘Published posts: ’

rmdes added 9 commits October 14, 2026 10:02
Add a post-type parameter to q=source, matching the Query for Post List
Micropub extension. Like filter and category, its value is cast to a
string before it reaches the database.
Add a form above the list of posts to choose a post type and search the
name and content, passed to the Micropub endpoint as post-type and
filter. Filters are kept in the next and previous page links.

Refs #843
Restores the private _count property added in f42a986 and dropped by
9bc6a35 when paging became cursor-based. The consumers in endpoint-posts
and endpoint-files were left in place and have read undefined since.

The count applies the same string-uid guard getCursor applies, so it never
reports posts that paging cannot reach.
Replaces the post type select with a row of links, one per configured post
type, built with the navigation component and a modifier class. Choosing a
post type is one click rather than a select and a submit, and the current
filter is visible without opening anything.

The text search moves into a details component, collapsed unless a search
term is in use, so the controls take one row when idle.
Adds a heading above the list stating how many posts are shown, and plays
the same text back in the document title so the information is available
without reading the page.

The heading states what the controls do not: the post type is marked on its
own link, so it is a prefix, while the search term sits in a collapsed
details and is played back. Any filter added later follows the same rule
without needing new sentences.

Templates take a documentTitle local to let the document title differ from
the page heading.
A key missing from a locale was rendered as the key itself, because
defaultLocale only covers an unknown locale, not a missing key within a
known one. Every locale file is complete today, so this covers the window
between a new key reaching en.json and Localazy supplying the rest.
The heading said "No posts match these filters" whatever the filters were,
so a page title carrying it did not say which search had failed. It now
names the search term, as it does when there are results, and the post type
prefix applies to every state rather than only to a non-empty list.

The other locales have not been given the interpolation yet, so they read
as the unqualified sentence until Localazy supplies it: true, but less
specific than the English.
@rmdes
rmdes force-pushed the feat/posts-filter-by-post-type branch from d04ab5c to 8a863d0 Compare October 14, 2026 08:58
@rmdes

rmdes commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

All three applied, one departure, one suggestion.

1. Details. The search is inside a details, collapsed unless a term is in use. The post types stay visible as a row of links (navigation with a --filters modifier, aria-current on the active one): one click, one row, and the current filter shows without opening anything. Easy to move inside if you'd rather.

Post type links above a collapsed search Filtered list with heading and open search

2. Heading and title. An h2 states what is shown; the <title> is the same text prefixed "Published posts: ". Rule: the heading says what the controls don't already show, so the type is a prefix ("Note · 613 posts", "Note · No posts matching “lunch”") rather than inside the sentence. A post type's name is a configured string, not a locale key, so "613 Note Beiträge" is what the inline form would give in German. Empty states follow your wording minus "found".

3. Localisation. Yes please to Localazy access. en.json has nine new keys; the five that don't interpolate are translated in the other fourteen files, the four that do (counts, filtered title) are English-only for now. A one-line retryInDefaultLocale makes a missing key fall back to English instead of printing the key path; an existing translation still wins.

4. "(showing posts 1 to 40)": left out, your call. Paging is cursor-based, so the offset would cost a second count per page for what next/previous already cover.

Noticed on the way: _count is yours (f42a986, 2022); 9bc6a35 removed it when paging went cursor-based but the consumers stayed, so endpoint-posts has read undefined since. Restored here with getCursor's string-uid guard. limit isn't carried into next/previous on main either; untouched.

@paulrobertlloyd

Copy link
Copy Markdown
Collaborator
  1. Let’s move the type filter inside the details. I also wonder if it should be a series of checkboxes? That would then allow users to filter by multiple post types.
  2. Good point
  3. Invitation to contribute on Localazy sent. I’d save adding localisations until the design is settled however.
  4. Makes sense, let’s consider that separately.

This branch has not been deployed

No deployments
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.

2 participants