결국 이를 위해 사용자 정의 테마 컴포넌트를 사용했습니다:
// A-Z 정렬을 위해 전체 카테고리가 필요합니다: 코어는 주제 목록을 제목으로 정렬할 수 없기 때문입니다
// (TopicQuery::SORTABLE_MAPPING에 "title"이 없으므로), 정렬 전에 페이지네이션된 목록을
// 모두 소진해야 합니다. 상한선이 설정되어 있습니다 — 문서 카테고리는 작으며, 더 큰 경우
// 로드된 항목만 정렬하는 방식으로 폴백하며, 아래 MutationObserver가 이를 최신 상태로 유지합니다.
// ponytail: 코어가 서버 측 제목 정렬을 제공하게 되면 이 전체 루프를 제거하세요.
const MAX_DOC_PAGES = 12; // 페이지당 30개 기준으로 약 360개 주제
let docLoadRun = 0;
async function loadAllDocTopics(api) {
const run = ++docLoadRun;
// `controller:discovery/topics`는 현재 Discourse에서 비추천(deprecated)입니다
// (비추천 ID: discourse.discovery-topics-controller). Discovery는 이제 필터별로
// 전용 컨트롤러를 사용합니다. 문서 카테고리는 항상 최신 주제 목록이므로,
// 해당 컨트롤러를 직접 조회합니다. 여기서 실패하더라도 첫 페이지만 정렬하는 것이므로,
// 모든 단계는 선택 체이닝(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();
// 로드 중 이동하거나 목록이 더 이상 성장하지 않는 경우 — 어느 쪽이든 중지합니다.
if (run !== docLoadRun || (list.topics?.length ?? 0) === before) {
return;
}
}
}
// 문서 주제 목록을 A-Z로 정렬하며, 페이지의 <html lang>와 함께 collation을 수행합니다.
let docSortObservers = new Map();
function docSortLocale() {
const lang = document.documentElement.lang;
if (!lang) {
return undefined;
}
try {
Intl.getCanonicalLocales(lang); // 잘못된 태그는 RangeError를 발생시킵니다
return lang;
} catch {
return undefined; // 브라우저의 기본 로케일로 폴백합니다
}
}
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; // 이미 정렬됨 — 옵저버가 스스로를 재입력하는 것을 방지합니다
}
// 행 뒤에 오는 것보다 앞에 삽입하여, load-more 센티널이 마지막에 있도록 유지합니다.
const anchor = rows[rows.length - 1].nextSibling;
// 첫 번째 보이는 행을 고정하여, 그 위 삽입으로 스크롤이 튀지 않도록 합니다.
const pinned = rows.find((row) => row.getBoundingClientRect().bottom > 0);
const pinnedTop = pinned?.getBoundingClientRect().top;
// 옵저버를 일시 정지하여, 우리의 이동이 이 함수를 다시 진입하지 않도록 합니다.
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();
// flex/grid 컨테이너가 아니므로, 재배열은 DOM 노드를 이동해야 합니다 — CSS `order`는 무시됩니다.
document.querySelectorAll(".topic-list.doc-simple-mode .topic-list-body").forEach((body) => {
// 정렬 중 예외가 발생해도 목록이 항상 표시되도록 try/finally 사용.
try {
sortDocTopicList(body, locale);
} finally {
body.classList.add("docs-sorted");
}
// 로딩이 안정될 때만 한 번 정렬합니다 — 로딩 중 재배열은 로더를 멈추게 했습니다.
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);
});
}
이 코드는 아마도 개선될 수 있을 것 같으며, 제안과/또는 PR을 기꺼이 환영합니다.