Skip to content

Real card views (Clarity) + upvotes (giscus/Discussions) - #52

Open
soyalejolopez wants to merge 5 commits into
masterfrom
alejanl-microsoft-verify-card-views-upvotes
Open

Real card views (Clarity) + upvotes (giscus/Discussions)#52
soyalejolopez wants to merge 5 commits into
masterfrom
alejanl-microsoft-verify-card-views-upvotes

Conversation

@soyalejolopez

Copy link
Copy Markdown
Owner

What

Replaces the fabricated view/upvote counters on the resource cards with real data.

Views -> Microsoft Clarity

  • build-stats.js pulls the Clarity Data Export API (URL dimension), accumulated daily and overlap-safe into traffic-data/clarity-views.json.
  • GitHub traffic stays as a migration fallback so the 7 already-real cards never regress.
  • Adds hash routing (index.html#<slug>) so Clarity attributes views per resource; deep-links + back/forward work.

Upvotes -> GitHub Discussions + giscus

  • Detail modal mounts a live giscus embed (category "Resource Votes", specific term = slug), theme-synced to the dark toggle, re-mounted per open.
  • Cards show a baked upvote count (GraphQL THUMBS_UP) only when a discussion exists; giscus lazily creates each thread on first reaction.
  • Legacy hash-based fake generator and the Issues-based path fully removed.

Verified

  • Headless smoke test: 0 app console errors; exactly 7 real view pills (powerclaw 1.2k, purview 656, ...); votes hidden until threads exist; deep-link #powerclaw-agent opens the modal; giscus resolves (repo-id/category-id) and reports lazy-create.

Required before all-card views light up

  • Add repo secret CLARITY_API_TOKEN (Settings -> Secrets and variables -> Actions).
  • Clarity coverage is forward-looking: it accumulates from deploy as visitors hit #slug URLs. Verify a few days in that Clarity records the URL fragment; if not, pivot to Clarity custom tags.

Co-authored-by: Copilot App

soyalejolopez and others added 3 commits July 21, 2026 08:11
#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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +251 to +288
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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;
}

Comment on lines +330 to +367
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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;
}

Comment on lines +170 to +173
if (metrics.uniques !== undefined && previous?.uniques !== undefined) {
const uniques = metrics.uniques - previous.uniques;
if (uniques >= 0) entry.uniques = Math.round(uniques);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);
    }

AnkitaDudeja and others added 2 commits July 21, 2026 12:41
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants