diff --git a/public/translations/ar/default.json b/public/translations/ar/default.json
index 3af1fe11..7db592cb 100644
--- a/public/translations/ar/default.json
+++ b/public/translations/ar/default.json
@@ -281,5 +281,6 @@
"show_blotter": "إظهار لوحة الإعلانات",
"top": "أعلى",
"reply_count": "عدد الردود",
- "pagination.pageCurrent": "الصفحة {{page}} (الحالية)"
+ "pagination.pageCurrent": "الصفحة {{page}} (الحالية)",
+ "pagination.pageLabel": "صفحة"
}
diff --git a/public/translations/bn/default.json b/public/translations/bn/default.json
index 6d6a64ce..8d2567cd 100644
--- a/public/translations/bn/default.json
+++ b/public/translations/bn/default.json
@@ -281,5 +281,6 @@
"show_blotter": "ঘোষণা দেখান",
"top": "শীর্ষ",
"reply_count": "উত্তরসংখ্যা",
- "pagination.pageCurrent": "পৃষ্ঠা {{page}} (বর্তমান)"
+ "pagination.pageCurrent": "পৃষ্ঠা {{page}} (বর্তমান)",
+ "pagination.pageLabel": "পৃষ্ঠা"
}
diff --git a/public/translations/cs/default.json b/public/translations/cs/default.json
index 3ce2cb71..60df989d 100644
--- a/public/translations/cs/default.json
+++ b/public/translations/cs/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Zobrazit Oznámení",
"top": "Nahoru",
"reply_count": "Počet odpovědí",
- "pagination.pageCurrent": "Stránka {{page}} (aktuální)"
+ "pagination.pageCurrent": "Stránka {{page}} (aktuální)",
+ "pagination.pageLabel": "Stránka"
}
diff --git a/public/translations/da/default.json b/public/translations/da/default.json
index 061d5b0d..8660dcd6 100644
--- a/public/translations/da/default.json
+++ b/public/translations/da/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Vis Meddelelser",
"top": "Top",
"reply_count": "Antal svar",
- "pagination.pageCurrent": "Side {{page}} (nuværende)"
+ "pagination.pageCurrent": "Side {{page}} (nuværende)",
+ "pagination.pageLabel": "Side"
}
diff --git a/public/translations/de/default.json b/public/translations/de/default.json
index c0d5ecd6..5c02a88b 100644
--- a/public/translations/de/default.json
+++ b/public/translations/de/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Ankündigungen anzeigen",
"top": "Oben",
"reply_count": "Anzahl Antworten",
- "pagination.pageCurrent": "Seite {{page}} (aktuell)"
+ "pagination.pageCurrent": "Seite {{page}} (aktuell)",
+ "pagination.pageLabel": "Seite"
}
diff --git a/public/translations/el/default.json b/public/translations/el/default.json
index 2aa1602a..23de1028 100644
--- a/public/translations/el/default.json
+++ b/public/translations/el/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Εμφάνιση Ανακοινώσεων",
"top": "Πάνω",
"reply_count": "Αριθμός απαντήσεων",
- "pagination.pageCurrent": "Σελίδα {{page}} (τρέχουσα)"
+ "pagination.pageCurrent": "Σελίδα {{page}} (τρέχουσα)",
+ "pagination.pageLabel": "Σελίδα"
}
diff --git a/public/translations/en/default.json b/public/translations/en/default.json
index d8d555a4..3baa4935 100644
--- a/public/translations/en/default.json
+++ b/public/translations/en/default.json
@@ -282,5 +282,6 @@
"message": "Message",
"show_all": "Show All",
"show_blotter": "Show Blotter",
- "reply_count": "Reply count"
+ "reply_count": "Reply count",
+ "pagination.pageLabel": "Page"
}
diff --git a/public/translations/es/default.json b/public/translations/es/default.json
index 052a9dad..23d0006b 100644
--- a/public/translations/es/default.json
+++ b/public/translations/es/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Mostrar Anuncios",
"top": "Arriba",
"reply_count": "Número de respuestas",
- "pagination.pageCurrent": "Página {{page}} (actual)"
+ "pagination.pageCurrent": "Página {{page}} (actual)",
+ "pagination.pageLabel": "Página"
}
diff --git a/public/translations/fa/default.json b/public/translations/fa/default.json
index 812d54b3..92cb5249 100644
--- a/public/translations/fa/default.json
+++ b/public/translations/fa/default.json
@@ -281,5 +281,6 @@
"show_blotter": "نمایش اعلانات",
"top": "بالا",
"reply_count": "تعداد پاسخها",
- "pagination.pageCurrent": "صفحه {{page}} (فعلی)"
+ "pagination.pageCurrent": "صفحه {{page}} (فعلی)",
+ "pagination.pageLabel": "صفحه"
}
diff --git a/public/translations/fi/default.json b/public/translations/fi/default.json
index ab291de3..f6caa509 100644
--- a/public/translations/fi/default.json
+++ b/public/translations/fi/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Näytä ilmoitukset",
"top": "Ylös",
"reply_count": "Vastausten määrä",
- "pagination.pageCurrent": "Sivu {{page}} (nykyinen)"
+ "pagination.pageCurrent": "Sivu {{page}} (nykyinen)",
+ "pagination.pageLabel": "Sivu"
}
diff --git a/public/translations/fil/default.json b/public/translations/fil/default.json
index 097ea00d..f43fd13c 100644
--- a/public/translations/fil/default.json
+++ b/public/translations/fil/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Ipakita ang Mga anunsyo",
"top": "Itaas",
"reply_count": "Bilang ng mga tugon",
- "pagination.pageCurrent": "Pahina {{page}} (kasalukuyan)"
+ "pagination.pageCurrent": "Pahina {{page}} (kasalukuyan)",
+ "pagination.pageLabel": "Pahina"
}
diff --git a/public/translations/fr/default.json b/public/translations/fr/default.json
index 958f5b12..63f4e702 100644
--- a/public/translations/fr/default.json
+++ b/public/translations/fr/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Afficher les annonces",
"top": "Haut",
"reply_count": "Nombre de réponses",
- "pagination.pageCurrent": "Page {{page}} (actuelle)"
+ "pagination.pageCurrent": "Page {{page}} (actuelle)",
+ "pagination.pageLabel": "Page"
}
diff --git a/public/translations/he/default.json b/public/translations/he/default.json
index ce6e8cd1..2c158e69 100644
--- a/public/translations/he/default.json
+++ b/public/translations/he/default.json
@@ -281,5 +281,6 @@
"show_blotter": "הצג הודעות",
"top": "למעלה",
"reply_count": "מספר תגובות",
- "pagination.pageCurrent": "עמוד {{page}} (נוכחי)"
+ "pagination.pageCurrent": "עמוד {{page}} (נוכחי)",
+ "pagination.pageLabel": "עמוד"
}
diff --git a/public/translations/hi/default.json b/public/translations/hi/default.json
index df5f768c..7645a0bb 100644
--- a/public/translations/hi/default.json
+++ b/public/translations/hi/default.json
@@ -281,5 +281,6 @@
"show_blotter": "सूचनाएं दिखाएं",
"top": "शीर्ष",
"reply_count": "जवाबों की संख्या",
- "pagination.pageCurrent": "पृष्ठ {{page}} (वर्तमान)"
+ "pagination.pageCurrent": "पृष्ठ {{page}} (वर्तमान)",
+ "pagination.pageLabel": "पृष्ठ"
}
diff --git a/public/translations/hu/default.json b/public/translations/hu/default.json
index 8e94f1ed..2921a1ac 100644
--- a/public/translations/hu/default.json
+++ b/public/translations/hu/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Közlemények megjelenítése",
"top": "Felül",
"reply_count": "Válaszok száma",
- "pagination.pageCurrent": "{{page}}. oldal (jelenlegi)"
+ "pagination.pageCurrent": "{{page}}. oldal (jelenlegi)",
+ "pagination.pageLabel": "Oldal"
}
diff --git a/public/translations/id/default.json b/public/translations/id/default.json
index d0825594..d6412bf2 100644
--- a/public/translations/id/default.json
+++ b/public/translations/id/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Tampilkan Pengumuman",
"top": "Atas",
"reply_count": "Jumlah balasan",
- "pagination.pageCurrent": "Halaman {{page}} (saat ini)"
+ "pagination.pageCurrent": "Halaman {{page}} (saat ini)",
+ "pagination.pageLabel": "Halaman"
}
diff --git a/public/translations/it/default.json b/public/translations/it/default.json
index e2bfeeb4..bfd1183a 100644
--- a/public/translations/it/default.json
+++ b/public/translations/it/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Mostra annunci",
"top": "In alto",
"reply_count": "Numero di risposte",
- "pagination.pageCurrent": "Pagina {{page}} (corrente)"
+ "pagination.pageCurrent": "Pagina {{page}} (corrente)",
+ "pagination.pageLabel": "Pagina"
}
diff --git a/public/translations/ja/default.json b/public/translations/ja/default.json
index 90dc7c0c..4cb4724e 100644
--- a/public/translations/ja/default.json
+++ b/public/translations/ja/default.json
@@ -281,5 +281,6 @@
"show_blotter": "お知らせを表示",
"top": "上",
"reply_count": "返信数",
- "pagination.pageCurrent": "{{page}}ページ(現在)"
+ "pagination.pageCurrent": "{{page}}ページ(現在)",
+ "pagination.pageLabel": "ページ"
}
diff --git a/public/translations/ko/default.json b/public/translations/ko/default.json
index a27f8e6e..94532d80 100644
--- a/public/translations/ko/default.json
+++ b/public/translations/ko/default.json
@@ -281,5 +281,6 @@
"show_blotter": "공지 표시",
"top": "맨 위",
"reply_count": "답글 수",
- "pagination.pageCurrent": "{{page}}페이지 (현재)"
+ "pagination.pageCurrent": "{{page}}페이지 (현재)",
+ "pagination.pageLabel": "페이지"
}
diff --git a/public/translations/mr/default.json b/public/translations/mr/default.json
index cef0bb8c..5724e3af 100644
--- a/public/translations/mr/default.json
+++ b/public/translations/mr/default.json
@@ -281,5 +281,6 @@
"show_blotter": "जाहिराती दाखवा",
"top": "वर",
"reply_count": "प्रत्युत्तरांची संख्या",
- "pagination.pageCurrent": "पृष्ठ {{page}} (वर्तमान)"
+ "pagination.pageCurrent": "पृष्ठ {{page}} (वर्तमान)",
+ "pagination.pageLabel": "पृष्ठ"
}
diff --git a/public/translations/nl/default.json b/public/translations/nl/default.json
index aaaa5392..64b1c0d9 100644
--- a/public/translations/nl/default.json
+++ b/public/translations/nl/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Mededelingen tonen",
"top": "Boven",
"reply_count": "Aantal reacties",
- "pagination.pageCurrent": "Pagina {{page}} (huidige)"
+ "pagination.pageCurrent": "Pagina {{page}} (huidige)",
+ "pagination.pageLabel": "Pagina"
}
diff --git a/public/translations/no/default.json b/public/translations/no/default.json
index 49d3a8da..3afadad9 100644
--- a/public/translations/no/default.json
+++ b/public/translations/no/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Vis Kunngjøringer",
"top": "Topp",
"reply_count": "Antall svar",
- "pagination.pageCurrent": "Side {{page}} (nåværende)"
+ "pagination.pageCurrent": "Side {{page}} (nåværende)",
+ "pagination.pageLabel": "Side"
}
diff --git a/public/translations/pl/default.json b/public/translations/pl/default.json
index 48d3807d..0701b1ef 100644
--- a/public/translations/pl/default.json
+++ b/public/translations/pl/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Pokaż Ogłoszenia",
"top": "Góra",
"reply_count": "Liczba odpowiedzi",
- "pagination.pageCurrent": "Strona {{page}} (bieżąca)"
+ "pagination.pageCurrent": "Strona {{page}} (bieżąca)",
+ "pagination.pageLabel": "Strona"
}
diff --git a/public/translations/pt/default.json b/public/translations/pt/default.json
index 5bd352ae..16cf2b2c 100644
--- a/public/translations/pt/default.json
+++ b/public/translations/pt/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Mostrar Anúncios",
"top": "Topo",
"reply_count": "Número de respostas",
- "pagination.pageCurrent": "Página {{page}} (atual)"
+ "pagination.pageCurrent": "Página {{page}} (atual)",
+ "pagination.pageLabel": "Página"
}
diff --git a/public/translations/ro/default.json b/public/translations/ro/default.json
index 203d3469..b1a31d88 100644
--- a/public/translations/ro/default.json
+++ b/public/translations/ro/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Afișează Blotter",
"top": "Sus",
"reply_count": "Număr de răspunsuri",
- "pagination.pageCurrent": "Pagina {{page}} (curentă)"
+ "pagination.pageCurrent": "Pagina {{page}} (curentă)",
+ "pagination.pageLabel": "Pagină"
}
diff --git a/public/translations/ru/default.json b/public/translations/ru/default.json
index 05c47457..89425173 100644
--- a/public/translations/ru/default.json
+++ b/public/translations/ru/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Показать объявления",
"top": "Вверх",
"reply_count": "Количество ответов",
- "pagination.pageCurrent": "Страница {{page}} (текущая)"
+ "pagination.pageCurrent": "Страница {{page}} (текущая)",
+ "pagination.pageLabel": "Страница"
}
diff --git a/public/translations/sq/default.json b/public/translations/sq/default.json
index 04e92744..a23fd440 100644
--- a/public/translations/sq/default.json
+++ b/public/translations/sq/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Shfaq Njoftimet",
"top": "Lart",
"reply_count": "Numri i përgjigjeve",
- "pagination.pageCurrent": "Faqja {{page}} (aktuale)"
+ "pagination.pageCurrent": "Faqja {{page}} (aktuale)",
+ "pagination.pageLabel": "Faqe"
}
diff --git a/public/translations/sv/default.json b/public/translations/sv/default.json
index dab86a31..4db7e962 100644
--- a/public/translations/sv/default.json
+++ b/public/translations/sv/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Visa Meddelanden",
"top": "Topp",
"reply_count": "Antal svar",
- "pagination.pageCurrent": "Sida {{page}} (nuvarande)"
+ "pagination.pageCurrent": "Sida {{page}} (nuvarande)",
+ "pagination.pageLabel": "Sida"
}
diff --git a/public/translations/te/default.json b/public/translations/te/default.json
index 04667948..3bf1484e 100644
--- a/public/translations/te/default.json
+++ b/public/translations/te/default.json
@@ -281,5 +281,6 @@
"show_blotter": "ప్రకటనలు చూపించు",
"top": "పై",
"reply_count": "సమాధానాల సంఖ్య",
- "pagination.pageCurrent": "పేజీ {{page}} (ప్రస్తుత)"
+ "pagination.pageCurrent": "పేజీ {{page}} (ప్రస్తుత)",
+ "pagination.pageLabel": "పేజీ"
}
diff --git a/public/translations/th/default.json b/public/translations/th/default.json
index 7647eba8..00a0760b 100644
--- a/public/translations/th/default.json
+++ b/public/translations/th/default.json
@@ -281,5 +281,6 @@
"show_blotter": "แสดงประกาศ",
"top": "บน",
"reply_count": "จำนวนการตอบกลับ",
- "pagination.pageCurrent": "หน้า {{page}} (ปัจจุบัน)"
+ "pagination.pageCurrent": "หน้า {{page}} (ปัจจุบัน)",
+ "pagination.pageLabel": "หน้า"
}
diff --git a/public/translations/tr/default.json b/public/translations/tr/default.json
index 2aeb9962..de966b9e 100644
--- a/public/translations/tr/default.json
+++ b/public/translations/tr/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Duyuruları Göster",
"top": "Üst",
"reply_count": "Yanıt sayısı",
- "pagination.pageCurrent": "Sayfa {{page}} (mevcut)"
+ "pagination.pageCurrent": "Sayfa {{page}} (mevcut)",
+ "pagination.pageLabel": "Sayfa"
}
diff --git a/public/translations/uk/default.json b/public/translations/uk/default.json
index f5a2587b..9b352716 100644
--- a/public/translations/uk/default.json
+++ b/public/translations/uk/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Показати оголошення",
"top": "Вгору",
"reply_count": "Кількість відповідей",
- "pagination.pageCurrent": "Сторінка {{page}} (поточна)"
+ "pagination.pageCurrent": "Сторінка {{page}} (поточна)",
+ "pagination.pageLabel": "Сторінка"
}
diff --git a/public/translations/ur/default.json b/public/translations/ur/default.json
index f26db717..62437cfa 100644
--- a/public/translations/ur/default.json
+++ b/public/translations/ur/default.json
@@ -281,5 +281,6 @@
"show_blotter": "اعلانات دکھائیں",
"top": "اوپر",
"reply_count": "جوابوں کی تعداد",
- "pagination.pageCurrent": "صفحہ {{page}} (موجودہ)"
+ "pagination.pageCurrent": "صفحہ {{page}} (موجودہ)",
+ "pagination.pageLabel": "صفحہ"
}
diff --git a/public/translations/vi/default.json b/public/translations/vi/default.json
index ddd559eb..7ad325c6 100644
--- a/public/translations/vi/default.json
+++ b/public/translations/vi/default.json
@@ -281,5 +281,6 @@
"show_blotter": "Hiển thị Thông báo",
"top": "Trên",
"reply_count": "Số lượng phản hồi",
- "pagination.pageCurrent": "Trang {{page}} (hiện tại)"
+ "pagination.pageCurrent": "Trang {{page}} (hiện tại)",
+ "pagination.pageLabel": "Trang"
}
diff --git a/public/translations/zh/default.json b/public/translations/zh/default.json
index e599aa50..37f032be 100644
--- a/public/translations/zh/default.json
+++ b/public/translations/zh/default.json
@@ -281,5 +281,6 @@
"show_blotter": "显示公告",
"top": "顶部",
"reply_count": "回复数",
- "pagination.pageCurrent": "第{{page}}页(当前)"
+ "pagination.pageCurrent": "第{{page}}页(当前)",
+ "pagination.pageLabel": "页"
}
diff --git a/src/components/board-buttons/board-buttons.tsx b/src/components/board-buttons/board-buttons.tsx
index f30397bf..de58c127 100644
--- a/src/components/board-buttons/board-buttons.tsx
+++ b/src/components/board-buttons/board-buttons.tsx
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAccountComment, useComment, useSubscribe } from '@plebbit/plebbit-react-hooks';
import { isAllView, isCatalogView, isModView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
+import { usePostPageNumber } from '../../hooks/use-post-page-number';
import { useDirectories } from '../../hooks/use-directories';
import { getBoardPath, isDirectoryBoard } from '../../lib/utils/route-utils';
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
@@ -457,7 +458,11 @@ export const MobileBoardButtons = () => {
export const PostPageStats = () => {
const { t } = useTranslation();
const params = useParams();
+ const location = useLocation();
const commentCid = params?.commentCid as string | undefined;
+ const resolvedAddress = useResolvedSubplebbitAddress();
+ const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
+ const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress;
const comment = useComment({ commentCid });
const postCid = comment?.parentCid ?? commentCid;
@@ -466,6 +471,13 @@ export const PostPageStats = () => {
const { closed, pinned, replyCount } = post || {};
const linkCount = useCountLinksInReplies(post);
+ const isThreadView = isPostPageView(location.pathname, params);
+ const pageNumber = usePostPageNumber({
+ subplebbitAddress,
+ postCid,
+ enabled: isThreadView,
+ });
+
const displayReplyCount = replyCount !== undefined ? replyCount.toString() : '?';
const replyCountTooltip = replyCount !== undefined ? capitalize(t('replies')) : t('loading');
@@ -474,6 +486,12 @@ export const PostPageStats = () => {
{pinned && `${capitalize(t('sticky'))} / `}
{closed && `${capitalize(t('closed'))} / `}
{displayReplyCount} / {linkCount?.toString()}
+ {isThreadView && (
+ <>
+ {' '}
+ / {pageNumber?.toString() ?? '?'}
+ >
+ )}
);
};
diff --git a/src/hooks/use-post-page-number.ts b/src/hooks/use-post-page-number.ts
new file mode 100644
index 00000000..614d5cd8
--- /dev/null
+++ b/src/hooks/use-post-page-number.ts
@@ -0,0 +1,55 @@
+import { useMemo } from 'react';
+import { useFeed } from '@plebbit/plebbit-react-hooks';
+import useFeedsStore from '@plebbit/plebbit-react-hooks/dist/stores/feeds';
+import { useDirectoryByAddress } from './use-directories';
+import { useBoardFeedPageSize } from './use-board-feed-page-size';
+import { findPostPageInFeed, findPostPageInLoadedBoardFeeds, type FeedsOptionsLike, type LoadedFeedsLike } from '../lib/utils/post-page-resolution';
+
+export interface UsePostPageNumberOptions {
+ subplebbitAddress: string | undefined;
+ postCid: string | undefined;
+ /** When false, page segment is excluded (e.g. pending-post view). When true, resolve and show page. */
+ enabled?: boolean;
+}
+
+/**
+ * Resolve the board pagination page (1-based) for a post in a thread view.
+ * Uses cache-first lookup from feeds store, then preloads via useFeed if miss.
+ * Preload depth is capped to board pagination window (10 pages via paginationFeedPostsPerPage).
+ *
+ * @returns 1-based page number, or undefined when unresolved (render as "?")
+ */
+export function usePostPageNumber({ subplebbitAddress, postCid, enabled = true }: UsePostPageNumberOptions): number | undefined {
+ const community = useDirectoryByAddress(subplebbitAddress);
+ const { guiPostsPerPage, paginationFeedPostsPerPage } = useBoardFeedPageSize(community);
+
+ const canResolve = Boolean(enabled && subplebbitAddress && postCid && guiPostsPerPage > 0);
+
+ // Cache-first: selector returns only computed page to minimize rerenders
+ const cachedPage = useFeedsStore((state) => {
+ if (!canResolve) return undefined;
+ return findPostPageInLoadedBoardFeeds(state.feedsOptions as FeedsOptionsLike, state.loadedFeeds as LoadedFeedsLike, subplebbitAddress!, postCid!, guiPostsPerPage);
+ });
+
+ // Preload when cache miss and enabled (10 GUI pages)
+ const preloadOptions = useMemo(
+ () =>
+ canResolve
+ ? {
+ subplebbitAddresses: [subplebbitAddress!],
+ sortType: 'active' as const,
+ postsPerPage: paginationFeedPostsPerPage,
+ }
+ : undefined,
+ [canResolve, subplebbitAddress, paginationFeedPostsPerPage],
+ );
+
+ const { feed: preloadFeed } = useFeed(preloadOptions);
+
+ const preloadedPage = useMemo(() => {
+ if (!canResolve || !preloadFeed?.length) return undefined;
+ return findPostPageInFeed(preloadFeed, postCid!, guiPostsPerPage);
+ }, [canResolve, postCid, preloadFeed, guiPostsPerPage]);
+
+ return cachedPage ?? preloadedPage;
+}
diff --git a/src/lib/utils/post-page-resolution.ts b/src/lib/utils/post-page-resolution.ts
new file mode 100644
index 00000000..3b90be39
--- /dev/null
+++ b/src/lib/utils/post-page-resolution.ts
@@ -0,0 +1,96 @@
+/**
+ * Post page resolution utilities.
+ * Page semantics are board pagination pages (not catalog ordering, not fetch-chunk index).
+ */
+
+/** Minimal Comment shape with cid */
+export interface CommentWithCid {
+ cid?: string;
+ [key: string]: unknown;
+}
+
+/** Minimal FeedOptions shape for board-feed filtering */
+export interface FeedOptionsLike {
+ subplebbitAddresses: string[];
+ sortType: string;
+ postsPerPage?: number;
+ filter?: unknown;
+ newerThan?: number;
+ modQueue?: unknown;
+ accountComments?: unknown;
+}
+
+/** FeedsOptions-like map */
+export type FeedsOptionsLike = Record;
+
+/** Loaded feeds map: feedName -> Comment[] */
+export type LoadedFeedsLike = Record;
+
+/**
+ * Find the board pagination page (1-based) containing postCid in a feed.
+ * Pure index math: page = floor(index / guiPostsPerPage) + 1.
+ *
+ * @param feed - Array of comments (posts) in feed order
+ * @param postCid - CID of the post (OP) to locate
+ * @param guiPostsPerPage - Posts per GUI page
+ * @returns 1-based page number, or undefined if post not found
+ */
+export function findPostPageInFeed(feed: CommentWithCid[], postCid: string, guiPostsPerPage: number): number | undefined {
+ if (!postCid || guiPostsPerPage <= 0) return undefined;
+ const index = feed.findIndex((c) => c.cid === postCid);
+ if (index < 0) return undefined;
+ return Math.floor(index / guiPostsPerPage) + 1;
+}
+
+/**
+ * Strict board-feed filter criteria.
+ * A feed is a "board feed" iff:
+ * - sortType === 'active'
+ * - single-sub feed (subplebbitAddresses.length === 1)
+ * - no filter, no newerThan, no modQueue, no accountComments
+ */
+export function isBoardFeedOptions(opts: FeedOptionsLike, subplebbitAddress: string): boolean {
+ return (
+ opts.sortType === 'active' &&
+ opts.subplebbitAddresses?.length === 1 &&
+ opts.subplebbitAddresses[0] === subplebbitAddress &&
+ !opts.filter &&
+ opts.newerThan == null &&
+ !opts.modQueue &&
+ !opts.accountComments
+ );
+}
+
+/**
+ * Find the board pagination page for postCid by searching loaded board feeds.
+ * Only considers feeds matching strict board-feed criteria.
+ *
+ * @param feedsOptions - Feeds store feedsOptions
+ * @param loadedFeeds - Feeds store loadedFeeds
+ * @param subplebbitAddress - Board subplebbit address
+ * @param postCid - CID of the post (OP) to locate
+ * @param guiPostsPerPage - Posts per GUI page
+ * @returns 1-based page number, or undefined if not found in any matching feed
+ */
+export function findPostPageInLoadedBoardFeeds(
+ feedsOptions: FeedsOptionsLike,
+ loadedFeeds: LoadedFeedsLike,
+ subplebbitAddress: string,
+ postCid: string,
+ guiPostsPerPage: number,
+): number | undefined {
+ if (!subplebbitAddress || !postCid || guiPostsPerPage <= 0) return undefined;
+
+ for (const feedName of Object.keys(feedsOptions)) {
+ const opts = feedsOptions[feedName];
+ if (!opts || !isBoardFeedOptions(opts, subplebbitAddress)) continue;
+
+ const feed = loadedFeeds[feedName];
+ if (!feed || !Array.isArray(feed)) continue;
+
+ const page = findPostPageInFeed(feed, postCid, guiPostsPerPage);
+ if (page !== undefined) return page;
+ }
+
+ return undefined;
+}