gilles
27 augustus 2026 om 09:05
1
I’ve noticed that it’s not possible to sort a category alphabetically by default topic titles. I’m sure I’ll be told that I can just use the filters to make the adjustment, but that raises two major issues:
It requires too many steps, which isn’t within everyone’s reach.
You lose the usual tile layout.
Would it be possible to have a per-category parameter to define an alphabetical sort of topics by default? I’m not sure if this is planned in the roadmap.
I’ll repeat myself: this is just feedback from real-world experience, but I think it’s a shame to miss out on this option, which is standard on Discourse.
2 likes
chapoi
27 augustus 2026 om 10:32
2
Why would you want alphabetical ordering though? Whats the usecase you have in mind?
gilles
27 augustus 2026 om 10:35
3
Actually, in my categories, I have pages for board games, and I wanted the topics within these specific categories to be sorted alphabetically. I tried to do a little bit of development myself, but I just can’t figure it out.
2 likes
RGJ
(Richard - Communiteq)
27 augustus 2026 om 10:51
4
This is useful for courses and such where the topics are titled “Lesson 1”, “Lesson 2” etc.
Also, one of my biggest gripes with the doc-categories plugin is that it still exposes a topic list, especially on mobile. Being able to sort alphabetically would at least mitigate some of that.
4 likes
We are forcing a local A-Z render to keep our glossary clean. There are specific cases where it is really necessary.
Moin
27 augustus 2026 om 11:35
6
gilles:
You lose the usual tile layout.
I am not sure I understand this point. Which tiles are lost when looking at https://meta.discourse.org/filter?q=order%3Atitle-asc ?
1 like
gilles
27 augustus 2026 om 12:10
7
I may have expressed myself poorly in my email I thought we were keeping the same tile visuals
category without filter
category with filter
1 like
I can confirm that the filter is not useful for those of us who want to keep the default view as is, especially if it is applied to categories that function as Category Docs (Glossary, CMS).
It is a starting point, but it does not cover the simple need — perhaps not from a technical standpoint but certainly from our users perspective — to display topics organized alphabetically.
I think the filter covers other needs in a comprehensive way.
Just sharing a solution that I was using for some weeks:
I ended up using a custom Theme Component for it:
// A-Z needs the whole category: core cannot order a topic list by title
// (TopicQuery::SORTABLE_MAPPING has no "title"), so we exhaust the paginated list
// before sorting. Capped — docs categories are small; anything bigger falls back to
// sorting whatever is loaded, which the MutationObserver below keeps up to date.
// ponytail: drop this whole loop if core ever ships server-side title ordering.
const MAX_DOC_PAGES = 12; // ~360 topics at 30 per page
let docLoadRun = 0;
async function loadAllDocTopics(api) {
const run = ++docLoadRun;
// `controller:discovery/topics` is deprecated in current Discourse
// (deprecation id: discourse.discovery-topics-controller). Discovery now uses a
// dedicated controller per filter; docs categories are always the latest topic
// list, so we look that controller up directly. Failing here just means we sort
// the first page, so every step is optional-chained.
const list = api.container.lookup("controller:discovery/latest")?.model;
if (!list?.loadMore) {
return;
}
for (let page = 0; page < MAX_DOC_PAGES; page++) {
if (!list.more_topics_url) {
return;
}
const before = list.topics?.length ?? 0;
await list.loadMore();
// Navigated away mid-load, or the list stopped growing — either way, stop.
if (run !== docLoadRun || (list.topics?.length ?? 0) === before) {
return;
}
}
}
// Sorts the Docs topic list A-Z, collating with the page's own <html lang>.
let docSortObservers = new Map();
function docSortLocale() {
const lang = document.documentElement.lang;
if (!lang) {
return undefined;
}
try {
Intl.getCanonicalLocales(lang); // throws RangeError on a malformed tag
return lang;
} catch {
return undefined; // falls back to the browser's default locale
}
}
function sortDocTopicList(body, locale) {
const rows = [...body.querySelectorAll(":scope > .topic-list-item")];
if (rows.length < 2) {
return;
}
const sorted = [...rows].sort((a, b) => {
const titleA = a.querySelector(".title")?.textContent.trim() ?? "";
const titleB = b.querySelector(".title")?.textContent.trim() ?? "";
return titleA.localeCompare(titleB, locale);
});
if (sorted.every((row, i) => rows[i] === row)) {
return; // already sorted — stops the observer from feeding itself
}
// Insert before whatever follows the rows, keeping any load-more sentinel last.
const anchor = rows[rows.length - 1].nextSibling;
// Pin the first visible row so inserts above it don't jump the scroll.
const pinned = rows.find((row) => row.getBoundingClientRect().bottom > 0);
const pinnedTop = pinned?.getBoundingClientRect().top;
// Observer paused so our own moves don't re-enter this function.
const observer = docSortObservers.get(body);
observer?.disconnect();
sorted.forEach((row) => body.insertBefore(row, anchor));
observer?.observe(body, { childList: true });
if (pinned) {
window.scrollBy(0, pinned.getBoundingClientRect().top - pinnedTop);
}
}
function sortDocCategoryTopicLists() {
docSortObservers.forEach((observer) => observer.disconnect());
docSortObservers = new Map();
const locale = docSortLocale();
// Not a flex/grid container, so reordering must move DOM nodes — CSS `order` is ignored.
document.querySelectorAll(".topic-list.doc-simple-mode .topic-list-body").forEach((body) => {
// try/finally so the list is always revealed even if sorting throws.
try {
sortDocTopicList(body, locale);
} finally {
body.classList.add("docs-sorted");
}
// Sort only once loading goes quiet — mid-load reordering stalled the loader.
let settleTimer = null;
const observer = new MutationObserver(() => {
window.clearTimeout(settleTimer);
settleTimer = window.setTimeout(() => {
sortDocTopicList(body, locale);
}, 400);
});
observer.observe(body, { childList: true });
docSortObservers.set(body, observer);
});
}
I think the code can very likely be improved, I gladly accept suggestions and/or PRs.
1 like
gilles
14 september 2026 om 06:25
10
Great initiative, thanks @satonotdead
1 like