Given {period} (daily, weekly, or monthly), optional {language}, optional {spoken-language-code}, and one or more numeric {page} values, read GitHub Trending without mutations. Return each repository's page rank, owner, name, canonical URL, description, primary language, total stars, total forks, period-specific stars gained, language color, built-by contributors, all requested filters, final URL, and diagnostics. Verify whether GitHub retains every requested query parameter after navigation. When multiple pages are requested, compare canonical repository sets, deduplicate only when producing a stitched collection, and report distinct, duplicate, empty, blocked, or errored pages without assuming that a requested combination has results.
Use Cases
- Listing current all-language or language-specific trending repositories.
- Reading daily, weekly, or monthly Trending pages.
- Applying GitHub's spoken-language filter.
- Testing a specific numeric page, including page 0 or pages beyond the normal range.
- Verifying that
since,page,spoken_language_code, and the language path survive navigation, including case-preserving or case-normalized paths. - Testing whether pagination is genuine by comparing page 1 with a target or probe page.
- Returning normalized integer counts when GitHub displays K, M, or B abbreviations.
- Distinguishing an explicit empty result from a login wall, rate limit, CAPTCHA, changed layout, or duplicate page.
Automation Flow
- Normalize
{period}, optional{language}, optional{spoken-language-code}, target{page}, and optional comparison/probe pages. Default to page1; preserve the caller's language slug and URL-encode path components. Do not assume the filter combination has results. - Construct the direct URL without visiting the homepage or using controls:
https://github.com/trending/{language}?since={period}&page={page}&spoken_language_code={spoken-language-code}Omit the language path and spoken-language parameter only when those inputs are absent. A page-zero probe is allowed when requested: usepage=0exactly and classify its observed result. - Navigate directly to the target URL and run the evaluator below in the same browser call. For duplicate-page testing, also navigate directly to comparison pages such as page
1and run the same evaluator. Batch navigations and evaluations when supported; do not click filters, pagination controls, or repository links. - Use the final live URL, not merely the requested URL, as provenance. Parse its path and query parameters. Set
filter_retainedonly when the final URL retains the requestedsince, exact requested page string/value, optional spoken-language code, and the requested language identity case-insensitively; report both requested and observed values so case normalization is visible. - Treat a page with repository cards or explicit empty-state text as successful. A page with neither is blocked or structurally changed unless diagnostics establish otherwise. Preserve the normalized empty message and return count
0rather than fabricating repositories. - Compare canonical repository URL sets between requested pages. Identical sets are
duplicate_page; disjoint or partially different sets aredistinct(with overlap details). A page-zero or high-page result is classified from its final URL and extracted content, never from the page number alone. - Preserve one result per requested page, including final URL, observed filters, requested page, count, repositories, empty state, retention status, pagination status, and diagnostics. If producing a stitched collection, deduplicate by
(period, canonical repository URL)while retaining first-seen page and rank. Page-local ranks remain page-local. - Keep the operation read-only. Never star, fork, watch, or otherwise mutate repositories.
Run this deterministic extractor once on each loaded page:
(() => {
const clean = s => (s || '').replace(/\s+/g, ' ').trim();
const metric = value => {
const m = clean(value).match(/[\d,.]+\s*[KMB]?/i);
if (!m) return null;
const raw = m[0].replace(/,/g, '').replace(/\s/g, '');
const n = Number.parseFloat(raw);
if (Number.isNaN(n)) return null;
if (/b$/i.test(raw)) return Math.round(n * 1e9);
if (/m$/i.test(raw)) return Math.round(n * 1e6);
if (/k$/i.test(raw)) return Math.round(n * 1e3);
return Math.round(n);
};
const url = location.href;
const qs = new URL(url).searchParams;
const pathMatch = location.pathname.match(/^\/trending(?:\/([^/?#]+))?/i);
const observedLanguage = pathMatch?.[1] ? decodeURIComponent(pathMatch[1]) : null;
const pageRaw = qs.get('page');
const page = Number(pageRaw || '1');
const period = qs.get('since');
const spoken = qs.get('spoken_language_code');
const rows = [...document.querySelectorAll('article.Box-row')];
const repositories = rows.map((row, index) => {
const link = row.querySelector('h2 a[href^="/"]');
const href = link?.getAttribute('href') || '';
const parts = href.split('/').filter(Boolean);
const periodText = [...row.querySelectorAll('span')].map(e => clean(e.textContent)).find(t => /stars?\s+(today|this week|this month)/i.test(t)) || null;
const periodMatch = periodText?.match(/([\d,.]+(?:\s*[KMB])?)\s+stars?\s+(today|this week|this month)/i) || null;
const starLink = row.querySelector('a[href$="/stargazers"]');
const forkLink = row.querySelector('a[href$="/forks"]');
return {
rank: index + 1,
owner: parts[0] || null,
repository: parts[1] || null,
url: href ? new URL(href, location.origin).href : null,
description: clean(row.querySelector('p')?.textContent) || null,
language: clean(row.querySelector('[itemprop="programmingLanguage"]')?.textContent) || null,
language_color: row.querySelector('.repo-language-color')?.getAttribute('style')?.match(/#[0-9A-Fa-f]{6}/)?.[0] || null,
stars_total: metric(starLink?.textContent),
forks_total: metric(forkLink?.textContent),
stars_period: periodMatch ? metric(periodMatch[1]) : null,
period_label: periodMatch ? `stars ${periodMatch[2].toLowerCase()}` : null,
built_by: [...row.querySelectorAll('img[alt^="@"]').values()].map(img => clean(img.alt.slice(1))).filter(Boolean)
};
});
const empty_state = [...document.querySelectorAll('h1,h2,h3,p,[role="status"]')].map(e => clean(e.textContent)).find(t => /there(?:'|’)s|there are not|don(?:'|’)t have|no trending repositories|no repositories found|nothing trending/i.test(t) && /trending|repositories/i.test(t)) || null;
const diagnostics = [...document.querySelectorAll('[role="alert"], .flash, .js-flash-alert')].map(e => clean(e.textContent)).filter(Boolean).slice(0, 20);
const body = clean(document.body?.innerText);
const blocked = !repositories.length && !empty_state && /sign in|captcha|rate limit|access denied|not found|error/i.test(body);
const filters = {language: observedLanguage, since: period, page: pageRaw, spoken_language_code: spoken};
const paginationLinks = [...document.querySelectorAll('a[href]')].map(a => a.href).filter(h => /[?&]page=\d+/i.test(h)).slice(0, 20);
return {
ok: !blocked && (repositories.length > 0 || Boolean(empty_state)),
url,
title: document.title,
period,
language: observedLanguage,
spoken_language_code: spoken,
requested_page: page,
count: repositories.length,
repositories,
empty_state,
filters,
filter_retained: Boolean(period !== null && pageRaw !== null && (observedLanguage === null || /^\/trending\/[^/?#]+$/i.test(location.pathname))),
pagination_links: paginationLinks,
pagination_status: blocked ? 'blocked' : repositories.length ? 'unknown' : empty_state ? 'empty' : 'error',
diagnostics
};
})()After extraction, strengthen filter_retained in the caller using the original requested values: compare since, page number/string semantics, spoken-language code exactly, and language path case-insensitively. Do not mark retention true merely because some query parameters exist.
Possible Friction Points
- Direct Trending URLs are
/trending?since={period}&page={page}and/trending/{language}?since={period}&page={page}; homepage navigation and filter clicks are unnecessary. - The optional spoken-language query parameter is exactly
spoken_language_code={spoken-language-code}. Preserve it in output even when the result is empty. - GitHub may preserve or normalize the case of the language path. Compare language identity case-insensitively, but return the exact final URL and observed path.
- Repository cards are conventionally
article.Box-row; identity comes from theh2 ahref, while total stars and forks come from links ending in/stargazersand/forks. - The period-specific metric is separate from total stars and is rendered as
stars today,stars this week, orstars this month. - Counts may use commas or K/M/B suffixes; normalize only values actually present.
- Built-by contributors use image alt text beginning with
@; remove the prefix while preserving DOM order. - A page with no cards is not automatically empty. Require explicit empty-state text and return it; report login walls, rate limits, CAPTCHA, access denial, and structural changes as failures.
- Numeric pagination uses the
pagequery parameter. Page0and large page values may be empty, normalized, redirected, duplicated, or otherwise unsupported; classify only from observed content and final URL. - Successful navigation and retained parameters do not prove that pagination is genuine. Compare canonical repository URL sets. Identical sets must be reported as
duplicate_page. - Page-local rank is not a global rank; rank deltas are meaningful only for repositories present on both compared pages.
- CSS classes can change. Prefer repository hrefs,
itemprop="programmingLanguage", metric-link suffixes, semantic status elements, and the live URL. - Keep the workflow read-only: never star, fork, watch, or otherwise mutate repositories.