Real card views (Clarity) + upvotes (giscus/Discussions) - #52
Real card views (Clarity) + upvotes (giscus/Discussions)#52soyalejolopez wants to merge 5 commits into
Conversation
#48) Promote the new Linear-style resource catalog to the site root (index.html) and introduce a front-matter -> catalog.json -> site data pipeline so contributions flow efficiently into both the repo and the page. Site (index.html) - Modern, filterable catalog: comfortable + compact (table) densities, whole-card navigation, per-resource detail pages (what it is / why use it / how to use / prerequisites / author / version). - Engagement layer: views + upvotes with Popular / Trending / New / Engaging sorts. Microsoft branding, service description + contact links. - Microsoft Clarity analytics preserved; reduced-motion respected. Metadata pipeline - YAML front-matter in each resource README (authorship + semver versioning; git history is authoritative). - tools/catalog-build generator produces catalog.json (schema in docs/CATALOG-METADATA.md); the site fetches it at runtime. - .github/workflows/build-catalog.yml validates front-matter on PRs and regenerates catalog.json on push to master. - CONTRIBUTING.md + TEMPLATE-README.md updated with the new requirements. - Seeded front-matter across 30 resources. Copilot-Session: b0067184-a71b-474a-98d0-6d8009659085 Co-authored-by: soyalejolopez <88358406+soyalejolopez@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
#49) The 'Commit generated catalog' step failed on push to master with 'fatal: pathspec design-concepts/catalog.json did not match any files' because the design-concepts/ prototype folder is intentionally not part of the public repo. - Workflow: only stage/commit the root catalog.json; add a paths filter to the push trigger so publish runs only on catalog-relevant changes; add [skip ci] to the bot commit. - Generator: derive generatedAt from the newest resource 'updated' date instead of build time, so catalog.json is deterministic and the publish job only commits when resource content actually changes (no churn/loops). - Regenerated catalog.json (30 resources). Copilot-Session: b0067184-a71b-474a-98d0-6d8009659085 Co-authored-by: soyalejolopez <88358406+soyalejolopez@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Views now come from Microsoft Clarity (Data Export API, accumulated daily, overlap-safe) with GitHub traffic as a migration fallback. Upvotes come from GitHub Discussions reactions via an inline giscus embed in the detail modal, with baked THUMBS_UP counts on cards. Adds hash routing (#slug) so Clarity can attribute views per resource. Removes the hash-based fake view/upvote generator. - tools/catalog-build/build-stats.js: Clarity + Discussions stats pipeline - tools/catalog-build/create-vote-discussions.js: bootstrap vote discussions - index.html: giscus modal embed, hash routing, honest stat rendering (no data = no pill) - workflows: regenerate resource-stats.json on deploy + daily (needs CLARITY_API_TOKEN secret) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 511352f4-288b-47a4-861a-5861fc21b8f0
There was a problem hiding this comment.
Code Review
This pull request replaces the custom upvote system with a Giscus-based GitHub Discussions integration, updates the gallery routing to use URL hashes instead of query parameters, and introduces build scripts (build-stats.js and create-vote-discussions.js) to fetch and compile resource views and upvotes. Feedback on the new scripts highlights critical issues: collectDiscussions needs to return null instead of an empty array on failure to prevent wiping out existing upvote counts, the main loop should fall back to existing stats when upvote data is unavailable, and subtractClarityTotals should default previous uniques to zero to avoid omitting unique view counts for new resources.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| async function collectDiscussions() { | ||
| const discussions = []; | ||
| if (!githubToken) { | ||
| console.log('GITHUB_TOKEN or GH_TOKEN is not set; skipping discussion upvotes.'); | ||
| return discussions; | ||
| } | ||
|
|
||
| const query = `query($owner:String!,$name:String!,$after:String) { | ||
| repository(owner:$owner,name:$name) { | ||
| discussions(first:100,after:$after) { | ||
| pageInfo { hasNextPage endCursor } | ||
| nodes { | ||
| number | ||
| url | ||
| title | ||
| category { name } | ||
| reactionGroups { content reactors { totalCount } } | ||
| } | ||
| } | ||
| } | ||
| }`; | ||
| let after = null; | ||
| try { | ||
| do { | ||
| const data = await githubGraphql(query, { owner: 'microsoft', name: 'FastTrack', after }); | ||
| const connection = data?.repository?.discussions; | ||
| if (!connection) throw new Error('GitHub did not return the discussions connection'); | ||
| discussions.push(...connection.nodes.filter( | ||
| discussion => discussion?.category?.name === 'Resource Votes' | ||
| )); | ||
| after = connection.pageInfo.hasNextPage ? connection.pageInfo.endCursor : null; | ||
| } while (after); | ||
| } catch (error) { | ||
| console.warn(`Warning: could not collect GitHub Discussions: ${error.message}`); | ||
| return []; | ||
| } | ||
| return discussions; | ||
| } |
There was a problem hiding this comment.
If GITHUB_TOKEN is not set or if the GitHub API call fails, collectDiscussions currently returns an empty array []. This causes upvoteTotals to be empty, which subsequently overwrites and wipes out all existing upvote counts in resource-stats.json during the build. Returning null instead of [] allows the script to distinguish between a failed/skipped fetch and a successful fetch with zero discussions, enabling a fallback to the existing upvote counts.
async function collectDiscussions() {
if (!githubToken) {
console.log('GITHUB_TOKEN or GH_TOKEN is not set; skipping discussion upvotes.');
return null;
}
const query = `query($owner:String!,$name:String!,$after:String) {
repository(owner:$owner,name:$name) {
discussions(first:100,after:$after) {
pageInfo { hasNextPage endCursor }
nodes {
number
url
title
category { name }
reactionGroups { content reactors { totalCount } }
}
}
}
}`;
const discussions = [];
let after = null;
try {
do {
const data = await githubGraphql(query, { owner: 'microsoft', name: 'FastTrack', after });
const connection = data?.repository?.discussions;
if (!connection) throw new Error('GitHub did not return the discussions connection');
discussions.push(...connection.nodes.filter(
discussion => discussion?.category?.name === 'Resource Votes'
));
after = connection.pageInfo.hasNextPage ? connection.pageInfo.endCursor : null;
} while (after);
} catch (error) {
console.warn(`Warning: could not collect GitHub Discussions: ${error.message}`);
return null;
}
return discussions;
}| const clarityState = readOptionalJson(clarityStatePath, { lastRun: '', days: {} }); | ||
| if (!clarityState.days || typeof clarityState.days !== 'object') clarityState.days = {}; | ||
|
|
||
| const clarityChanged = await updateClarityState(clarityState, knownSlugs); | ||
| const clarityViews = collectClarityViews(clarityState, knownSlugs); | ||
| const fallbackViews = collectFallbackViews(); | ||
| const discussions = await collectDiscussions(); | ||
| const upvoteTotals = mapDiscussions(discussions, knownSlugs, discussionConfig.map ?? {}); | ||
| const resources = {}; | ||
| let viewCount = 0; | ||
| let upvoteCount = 0; | ||
|
|
||
| for (const resource of catalog.resources) { | ||
| const stats = {}; | ||
| const clarity = clarityViews.get(resource.slug); | ||
| let fallback; | ||
| try { | ||
| fallback = fallbackViews.get(new URL(resource.url).pathname); | ||
| } catch { | ||
| console.warn(`Warning: could not parse URL for ${resource.slug}.`); | ||
| } | ||
|
|
||
| // Clarity is authoritative once it has a nonzero cumulative total; legacy GitHub | ||
| // path traffic only prevents existing resources from losing views during migration. | ||
| const views = clarity?.views > 0 ? clarity : fallback; | ||
| if (views) { | ||
| Object.assign(stats, views); | ||
| viewCount += 1; | ||
| } | ||
|
|
||
| const upvotes = upvoteTotals.get(resource.slug); | ||
| if (upvotes) { | ||
| Object.assign(stats, upvotes); | ||
| upvoteCount += 1; | ||
| } | ||
|
|
||
| if (Object.keys(stats).length > 0) resources[resource.slug] = stats; | ||
| } |
There was a problem hiding this comment.
To prevent wiping out existing upvote counts when the GitHub Discussions fetch is skipped or fails, we should load the existing resource-stats.json and fall back to its upvote and discussion values if upvoteTotals is null.
const clarityState = readOptionalJson(clarityStatePath, { lastRun: '', days: {} });
if (!clarityState.days || typeof clarityState.days !== 'object') clarityState.days = {};
const clarityChanged = await updateClarityState(clarityState, knownSlugs);
const clarityViews = collectClarityViews(clarityState, knownSlugs);
const fallbackViews = collectFallbackViews();
const discussions = await collectDiscussions();
const upvoteTotals = discussions !== null ? mapDiscussions(discussions, knownSlugs, discussionConfig.map ?? {}) : null;
const existingStats = readOptionalJson(join(repositoryRoot, 'resource-stats.json'), { resources: {} });
const resources = {};
let viewCount = 0;
let upvoteCount = 0;
for (const resource of catalog.resources) {
const stats = {};
const clarity = clarityViews.get(resource.slug);
let fallback;
try {
fallback = fallbackViews.get(new URL(resource.url).pathname);
} catch {
console.warn(`Warning: could not parse URL for ${resource.slug}.`);
}
// Clarity is authoritative once it has a nonzero cumulative total; legacy GitHub
// path traffic only prevents existing resources from losing views during migration.
const views = clarity?.views > 0 ? clarity : fallback;
if (views) {
Object.assign(stats, views);
viewCount += 1;
}
const upvotes = upvoteTotals !== null
? upvoteTotals.get(resource.slug)
: (existingStats.resources?.[resource.slug]?.upvotes !== undefined ? {
upvotes: existingStats.resources[resource.slug].upvotes,
discussion: existingStats.resources[resource.slug].discussion
} : undefined);
if (upvotes) {
Object.assign(stats, upvotes);
upvoteCount += 1;
}
if (Object.keys(stats).length > 0) resources[resource.slug] = stats;
}| if (metrics.uniques !== undefined && previous?.uniques !== undefined) { | ||
| const uniques = metrics.uniques - previous.uniques; | ||
| if (uniques >= 0) entry.uniques = Math.round(uniques); | ||
| } |
There was a problem hiding this comment.
In subtractClarityTotals, if a resource is new or only has data in the larger window (meaning previous is undefined), the unique views count (entry.uniques) is completely omitted because of the strict previous?.uniques !== undefined check. Defaulting the previous uniques to 0 ensures that the unique count is correctly calculated and preserved.
if (metrics.uniques !== undefined) {
const uniques = metrics.uniques - (previous?.uniques ?? 0);
if (uniques >= 0) entry.uniques = Math.round(uniques);
}Combines upstream PR microsoft#477's self-contained iframe viewer with this branch's real views (Clarity + traffic), giscus-based upvotes, and per-resource hash routing. Resolves conflicts in index.html, build-catalog.yml, and package.json. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 511352f4-288b-47a4-861a-5861fc21b8f0
collectFallbackViews now normalizes GitHub traffic paths to repo-relative folders (handling /tree/ vs /blob/) and credits each path to its longest-matching resource folder, so a resource's own files and subfolders count toward its historical view seed. No ancestor/sibling double-counting. powerclaw-agent +220 uniques (SETUP.md + skill file); viva dashboard +16. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 511352f4-288b-47a4-861a-5861fc21b8f0
What
Replaces the fabricated view/upvote counters on the resource cards with real data.
Views -> Microsoft Clarity
build-stats.jspulls the Clarity Data Export API (URL dimension), accumulated daily and overlap-safe intotraffic-data/clarity-views.json.index.html#<slug>) so Clarity attributes views per resource; deep-links + back/forward work.Upvotes -> GitHub Discussions + giscus
specificterm = slug), theme-synced to the dark toggle, re-mounted per open.THUMBS_UP) only when a discussion exists; giscus lazily creates each thread on first reaction.Verified
#powerclaw-agentopens the modal; giscus resolves (repo-id/category-id) and reports lazy-create.Required before all-card views light up
CLARITY_API_TOKEN(Settings -> Secrets and variables -> Actions).#slugURLs. Verify a few days in that Clarity records the URL fragment; if not, pivot to Clarity custom tags.Co-authored-by: Copilot App