Redesign livepeer.org - #93
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The register cut by the body answerable for each commitment: every owner as a heading linking to its page, its commitments beneath with what needs attention first — off track, at risk, silent, on track, then committed work with no health yet by target — and the bodies ordered by the worst thing under them, so the page opens on whoever has something to answer for. In the URL as ?group=owner, so it is a link you can send. Roadmap only; Shipped keeps its months. The control sits at the far end of the view tabs from lg up and inside the Filters panel below that, where a second line in the tab row would have slid the sticky bands under it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Shipped by owner is a track record per body, most recent first. The "Nothing is committed past …" line answers when, so it shows under the quarter cut and not the owner cut, which is answering who. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It’s a large, cross-cutting redesign (routing, metadata/sharing, content sources, and new endpoints), so it needs careful human validation beyond the specific issues flagged.
Review details
Suppressed comments (2)
app/ecosystem/submit/page.tsx:51
fs.readFileSyncruns during render, so this page will re-readcontent/ecosystem-template.mdevery time it’s requested (or every time the server component is evaluated). Since the template is static, cache the computed GitHub “new file” URL at module scope (or behind a simple in-module memo) to avoid repeated synchronous I/O.
app/people/[slug]/opengraph-image.tsx:22- This people share-card is rendered with
ogArt.organizations, which means people pages will share the Organizations background art (and any future changes intended for org pages will also affect people). If this is accidental (copy/paste from the org route), consider adding a dedicatedogArt.peopleentry inlib/og.tsx(or selecting an existing non-org art constant) and using it here.
- Files reviewed: 127/338 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
The change set is a full-site redesign with many new routes/components and content model changes, so it warrants final human validation despite only a couple of concrete issues found in the reviewed hunks.
Review details
Suppressed comments (1)
app/people/[slug]/opengraph-image.tsx:22
- This person card uses
ogArt.organizations, which will make every /people/ share image look like an organization page. There isn’t a dedicated people art token inlib/og.tsx; using the roadmap art is a closer match for people being a roadmap-adjacent surface.
- Files reviewed: 127/338 changed files
- Comments generated: 0 new
- Review effort level: Lite
The four words of the scale with their counts, always all four — Off track 0 is news — read as a filter the way Linear filters projects by health. It doubles as the tally the accountability board kept in its footer, and puts the silent ones one click away. Roadmap only, in the URL as ?health=, dropped on the way to Shipped like the state flag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The change set is a broad redesign touching routing, UI primitives, and dynamic data/metadata surfaces, and it includes at least one correctness issue that should be fixed and then re-verified end-to-end.
Review details
Suppressed comments (1)
app/ecosystem/[slug]/opengraph-image.tsx:17
- This OG image implementation renders the same generic
renderArtCard(ogArt.ecosystem)for every project. That contradicts the PR description (“All 12 projects now get their own titled card”) and makes different ecosystem project links indistinguishable in timelines.
Use renderTitledCard(...) and the project’s name (from the slug) so each project gets a unique share image.
- Files reviewed: 127/338 changed files
- Comments generated: 0 new
- Review effort level: Lite
A route and a tick for the two views, a calendar and a building for the two cuts, the way Linear marks its filters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The change set is a broad site redesign spanning routing, content sourcing, build/runtime behavior, and security-sensitive endpoints, which warrants focused human validation beyond automated review.
Review details
Suppressed comments (2)
app/api/revalidate/route.ts:2
- The token check should use a constant-time comparison;
timingSafeEqual(node:crypto) is a straightforward way to do that, but it needs to be imported.
app/api/revalidate/route.ts:47 - Bearer token validation uses a plain string comparison (
offered !== secret), which can leak information via timing differences. Compare in constant time (and check equal length) before allowing revalidation.
- Files reviewed: 127/338 changed files
- Comments generated: 1
- Review effort level: Lite
| <a | ||
| key={item.label} | ||
| href={item.href} | ||
| target="_blank" | ||
| rel="noreferrer" | ||
| onClick={close} | ||
| className={className} | ||
| > |
There was a problem hiding this comment.
🟡 Changes recommended
There are user-visible regressions/discrepancies in the ecosystem surface (share cards not titled per project and loss of URL-deep-linking for filters/search) that should be resolved before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
app/ecosystem/[slug]/opengraph-image.tsx:17
- This OpenGraph image generator returns renderArtCard(ogArt.ecosystem), so every ecosystem project page gets the same lockup-only card with no project title. The PR description states each project should get its own titled card; as written, shared links won’t be distinguishable in a timeline.
- Files reviewed: 127/338 changed files
- Comments generated: 1
- Review effort level: Lite
| export default function EcosystemPage() { | ||
| const apps: EcosystemListingApp[] = getAllApps().map((app) => ({ | ||
| slug: app.slug, | ||
| name: app.name, | ||
| url: app.url, | ||
| hostname: app.hostname, | ||
| displayUrl: app.displayUrl, | ||
| description: app.description, | ||
| categories: app.categories, | ||
| logo: app.logo, | ||
| logoBg: app.logoBg, | ||
| logoMonochrome: app.logoMonochrome, | ||
| })); | ||
| const categories = getEcosystemCategories(); | ||
|
|
||
| return ( | ||
| <EcosystemListingClient | ||
| <EcosystemListing | ||
| apps={apps} | ||
| categories={categories} | ||
| initialCategories={ | ||
| catsParam ? catsParam.split(",").map(decodeURIComponent) : [] | ||
| } | ||
| initialSearch={q ?? ""} | ||
| categories={getEcosystemCategories()} | ||
| heading={ecosystem.heading} | ||
| searchPlaceholder={ecosystem.searchPlaceholder} | ||
| emptyMessage={ecosystem.emptyMessage} | ||
| submitLabel={ecosystem.submitLabel} | ||
| submitHref={ecosystem.submitHref} | ||
| /> |
A post on a commitment now has a kind: the monthly update while the work is under way, or the one retrospective once it has shipped. Marked in Notion with a Kind select rather than inferred from its date, because a retro is often written the day something ships and an "it's out" update lands after; a retrospective carries no health, since "on track" means nothing about something finished. - Shipped cards read "Retro done" or "No retro" after the state. - A shipped record leads with its retrospective, or with the fact that none has been written, and the activity log runs in date order so the retro sits where it happened rather than under a pinned "shipped it". - The changelog's shipped rows carry the retro's line where one existed by the month's end, and roundup.json lists the month's shipped work without a retro as noRetro beside the silent. - Both readers accept the kind; the fallback gains a seed retro. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The tick disc and "Retrospective" where one exists, a hollow ring and "No retrospective" in the foreground where not. Not a checkbox, which on a card looks clickable; this is a fact. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The PR is a large WIP site redesign touching many routes, content sources, and metadata/share-image behaviors, so it needs targeted manual validation of key user flows and deploy/runtime behavior beyond automated review.
Review details
Suppressed comments (3)
app/ecosystem/[slug]/opengraph-image.tsx:17
- This project share-image route renders an art-only card (renderArtCard), but the PR description says ecosystem project pages now get a titled card. As written, the OG image will not include the project name, so shared links are harder to distinguish in a feed.
app/api/revalidate/route.ts:46 - The bearer token check uses a direct string comparison (
offered !== secret). For secret validation endpoints, a constant-time compare is safer to avoid leaking information via timing differences.
app/changelog/roundup.json/route.ts:54 - The JSON payload always advertises
url: https://livepeer.org/changelog/<month>, butroundup.jsondefaults to the current month (r.current=true) and the changelog month pages intentionally do not publish the current month. For the default case, this URL will point at a 404.
- Files reviewed: 127/339 changed files
- Comments generated: 0 new
- Review effort level: Lite
In medium weight above the muted write-up, in the log and on the card, so it reads as the lead rather than a paragraph that forgot its colour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The row's header has already said who, what and when, and a second title fought the write-up's bold lead-ins. The card keeps the title treatment, having no header row to lean on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Weight alone left it level with the write-up's bold lead-ins; at the body's size it was a paragraph in a different colour. One step up, in the log and on the card. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Several routes hard-disable dynamic params for Notion-backed records (risking permanent 404s for newly added rows) and there is at least one inconsistent shipped date that can misgroup changelog output.
Review details
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
app/organizations/[slug]/page.tsx:25
dynamicParams = falseprevents organization pages for new org rows added in Notion after the last build, even if the roadmap starts crediting/owning work under that org. Leaving dynamic params enabled keeps unknown slugs 404ing vianotFound()while allowing new orgs to be served on first request.
app/people/[slug]/page.tsx:19- With
dynamicParams = false, any new person added in Notion after the last build will 404 permanently (until a rebuild), even if commitments start linking to them. Consider leaving dynamic params enabled and relying onnotFound()for unknown slugs so the People table can grow without a deploy.
app/roadmap/[slug]/page.tsx:32 dynamicParams = falsemeans any new commitment added in Notion after the last build can appear on /roadmap but its detail page will still 404 because it wasn’t ingenerateStaticParams()at build time. Leaving dynamic params enabled allows new slugs to be served on first request while still 404ing unknown slugs vianotFound().
- Files reviewed: 127/339 changed files
- Comments generated: 0 new
- Review effort level: Lite
The expanded body started on the header's line and the title's leading was tight. More above the title, a looser line, a beat more below. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
There’s at least one confirmed functional issue (checkbox ::after never renders without content) plus a verified mismatch between the PR description and the ecosystem project OG card behavior (art-only vs titled card).
Review details
Suppressed comments (1)
app/ecosystem/[slug]/opengraph-image.tsx:17
- The PR description says ecosystem project pages now get “their own titled card”, but this opengraph image currently renders the generic art-only card (no project title). Either update the description or render a titled card using the project’s name (similar to blog post cards).
- Files reviewed: 127/339 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
This is a wide-ranging site redesign touching many routes/content systems, and there are unresolved review findings (security + UI class issue) that should be addressed alongside final human verification.
Review details
Suppressed comments (1)
app/api/revalidate/route.ts:46
- The bearer token check uses a direct string comparison (
offered !== secret), which is vulnerable to timing attacks. For a shared-secret auth check, use a constant-time comparison to avoid leaking information about the secret via response timing.
- Files reviewed: 127/339 changed files
- Comments generated: 0 new
- Review effort level: Lite
Tailwind's preflight resets a bare <button> to the arrow cursor, and the registry's Button sets the pointer, so the view tabs, the group-by radios, the two Clear filters buttons and the brand page's ink switch were the odd ones out. The header menu's two buttons are registry code and stay as they are. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The register can be drawn as a list: the same rows without their covers and teasers, one line each with the title, where it stands, the owner and the target in columns under a header row naming them, so the whole register fits on a screen. Linear's projects view; the cards stay the site's. ?display=list, a pair of glyphs after the group-by pair in the tab row, with their words under a facet heading in the Filters panel below lg. Below md the rows fold to a title over one muted line, each separator travelling with the field after it so a wrapped line never ends on a dot. A third grouping, by health: the four words of the scale as headings, worst first, then Committed, only headings with something under them. Roadmap view only; Shipped drops it on the way. Declined while cards were the only display, where a heading over one card repeats its mark; earns its place in the list. The rail narrows to 12rem with a 3rem gutter at lg (13rem and 5rem from xl) because the tab row with both pairs on it needs about 704px and the register had 656. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The toggle comes off the tab row and out of the Filters panel, and the rail goes back to 13rem with its 5rem gutter, which it only gave up to make room for the toggle. The rows and their header stay in code behind ?display=list, so the list can be tried by link and brought back with one control; the toggle is in 8192000. One way of reading the page for now. The cut by health stays. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The register's Target was free text ("Q4 2026") with a formula beside it
checking the format: nothing to pick from, a typo failed the build, and a
real date could not be stated at all. It is now the pair Linear stores:
Target date, from Notion's picker, and Target precision (Day, Month,
Quarter, Half-year, Year; empty means Quarter).
lib/target.ts turns the pair into the window the date falls in: its label
("Q3 2026", "July 2026", "September 15, 2026"), the quarter it sits under
when the roadmap is cut by quarter, so a day or a month does not get a
heading of its own, and a sort by the window's start then its end. Both
readers use it; the markdown copy carries targetDate and targetPrecision.
In Notion, all fourteen rows now hold the last day of their old window at
its old precision, checked to read back exactly as the text did. A Target
window formula shows leads the site's label, checked at all five
precisions, and replaces Target check. The views show the new properties,
and Target and Target check are dropped. No other branch reads them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Everything on the register is a commitment, so a status called Committed said nothing; it means one not started yet, which is Linear's Planned. The card's state word, the record's Status row, the organization page's section and the health cut's last heading all read Planned now. The reader maps both Notion option names to the same state until the option is renamed in Notion, so the two edits need not land together; Committed comes out of STATE_BY_NOTION after that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Status option is Planned in Notion, with every not-started row still on it, so the reader no longer needs to accept Committed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A row's health or Shipped moves from beside the title to the line under it, after the owner and the date: "Livepeer Foundation · Aug 27 · At risk". There it reads as what was said that day rather than what the item is now, which is what an entry is: the month as it ended. The silent rows read "Owner · No update" in the same place. Each middot is held to the field after it, so a wrapped line never ends on a dot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Inside an expanded Activity row the post's one-liner is set at the log's own size, since a size above outranked the row header it sits under, and the write-up runs entirely in the muted voice, bold lead-ins included, so the title is the only dark text and weight and colour carry the order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved security, cache invalidation, routing, validation, and share-card issues remain.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (10)
app/agent/opengraph-image.tsx:8
- This page still uses
renderArtCard, so its share image contains no page headline. The PR's share-image contract says every non-root page gets a titled card; switch this route torenderTitledCardwith the Agent page headline.
app/blog/category/[slug]/page.tsx:43 - Category pages define their own Open Graph/Twitter metadata, but this segment has no
opengraph-imageortwitter-imagefiles. That overrides the blog card without supplying a replacement, so/blog/category/<slug>has no share image; add a category image route or preserve the parent image metadata.
app/blog/opengraph-image.tsx:8 - The Latest/Blog landing card is still rendered with
renderArtCard, so it carries no page title despite the stated non-root share-card contract. Use the titled renderer with the page's current Latest headline.
app/brand/opengraph-image.tsx:8 - The Brand share image still calls
renderArtCard, leaving the card without any page-identifying headline. The stated share-image behavior requires a titled non-root card; userenderTitledCardwith the Brand page headline.
app/compute/opengraph-image.tsx:8 - The Compute page's share image is also an untitled
renderArtCard, so it does not identify the page in a feed. Replace it with a titled card using the Compute headline as required for non-root pages.
app/contribute/opengraph-image.tsx:8 - The Contribute page still emits only the artwork and lockup, with no page title. That leaves this non-root page outside the PR's stated titled-card behavior; use
renderTitledCardwith the Contribute headline.
app/ecosystem/[slug]/opengraph-image.tsx:16 - Project detail share images also call
renderArtCardwithout the project name, so all ecosystem project URLs produce the same indistinguishable card. Generate this image from the route slug withrenderTitledCard(using the project's name) so the promised per-project share card is actually distinct.
app/ecosystem/opengraph-image.tsx:8 - The ecosystem index's share image still uses the untitled renderer, so its card contains only the lockup and artwork instead of identifying the Ecosystem page. The PR's share-image contract says non-root pages carry their page title; use the titled renderer here.
app/foundation/opengraph-image.tsx:8 - The Foundation share image remains an untitled art card. This contradicts the PR's requirement that every non-root page image include its page title; render the Foundation headline with
renderTitledCardinstead.
app/token/opengraph-image.tsx:8 - The Token share image remains an untitled art card and therefore does not identify the page in social previews. The non-root share-image contract calls for a titled card; render the Token headline with
renderTitledCard.
- Files reviewed: 127/340 changed files
- Comments generated: 16
- Review effort level: Lite
| console.log( | ||
| `[notion-webhook] verification_token=${token}\n` + | ||
| `[notion-webhook] Save this as NOTION_WEBHOOK_SECRET, then paste it ` + | ||
| `into Notion's Webhooks tab to confirm the subscription. Events are ` + | ||
| `rejected until it is set.` |
| @@ -50,23 +63,27 @@ export function getAppBySlug(slug: string): EcosystemApp { | |||
| const { data, content } = matter(fileContents); | |||
| function readLinks(prop: Json | undefined, where: string): CommitmentLink[] { | ||
| const links = richText(prop).flatMap((run) => { | ||
| const label = (run.plain_text ?? "").trim(); | ||
| return run.href && label ? [{ label, href: run.href }] : []; |
| /** Only the register reads Notion, so this is the only path worth clearing. */ | ||
| const PATH = "/roadmap"; |
| /** Only the register reads Notion, so this is the only path worth clearing. */ | ||
| const PATH = "/roadmap"; |
| // One body per row, in parallel: fourteen small requests that would | ||
| // otherwise run end to end. | ||
| const details = await Promise.all( | ||
| rows.map((row) => | ||
| readDetail( | ||
| row.id as string, | ||
| `Roadmap commitments → ${text(props(row).Name)}` | ||
| ) | ||
| ) |
| cover: | ||
| coverProp?.type === "external" ? coverProp.external?.url : undefined, |
| cover: | ||
| coverProp?.type === "external" ? coverProp.external?.url : undefined, |
| function readCover(value: unknown, file: string): string | undefined { | ||
| if (value === undefined || value === null || value === "") return undefined; |
| const SELF_HOSTS = new Set(["livepeer.org", "www.livepeer.org"]); | ||
|
|
||
| const isSelf = (url) => { | ||
| try { | ||
| return SELF_HOSTS.has(new URL(url).hostname); | ||
| } catch { | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| const entries = readdirSync(ecosystemDir) | ||
| .filter((f) => f.endsWith(".md")) | ||
| .map((f) => matter(readFileSync(join(ecosystemDir, f), "utf-8")).data) | ||
| .filter((app) => app.url); | ||
|
|
||
| const skipped = entries.filter((app) => isSelf(app.url)); | ||
| const apps = entries.filter((app) => !isSelf(app.url)); |
Rebuilds the marketing site against Peace Node's Livepeer UI registry. The registry theme is now the only token layer.
Pages
Nine linked pages, all built from the mockup set except where noted:
//agent/ecosystem/[slug],/submit— markdown-driven catalog/compute/token/foundation/blog/blog/brand/roadmap/primerstays intact and unlinked with its own scoped legacy slice — the one page deliberately left unmigrated.Content is split on its shape and how often it changes. Page copy is authored in-repo as typed objects matching the registry's content contracts (
lib/site.ts) — versioned and reviewed alongside the design it belongs to. Blog and ecosystem are markdown incontent/.Templated content that gets updated frequently moves to Notion, and the roadmap register is the first of it. It is maintained by people across several organisations who do not open pull requests, and a commitment whose state is a week stale is worse than no page at all. The blog is planned next; scope is those two for now, not a general move of the site into a CMS. So this PR is the pattern as much as the page.
lib/notion.tsreads it over plainfetchwith ISR at a minute;content/roadmap/*.mdis the fallback when no workspace token is present, so a clone still builds. Every record carries an owner, a target and a checkable link, and anything missing one fails the build rather than rendering a card that claims less than it should.Cleanup
globals.css1209 → 585 lines. Removed the second--color-*token layer, the Holographik grid, hero classes,.blog-prose, 19 keyframes and 37 custom properties. Chart/sidebar/radius tokens are kept — unused today, but they're the registry's theme contract.Deleted ~73 files:
components/home/*,components/legacy/*,components/ecosystem/*(superseded bycomponents/livepeer-ui/*), the PascalCasecomponents/ui/*visuals,lib/constants.ts, and the fiveapp/use-cases/*routes — redirects innext.config.tsalready served those URLs, so the pages were unreachable.Share images
Rebuilt on the registry's own
ogitem./renders the brand card verbatim — the lockup centred on#000000. Every other page renders the same canvas with a page title, from one sharedlib/og.tsx. Two bugs fixed along the way:twitter-image, and metadata files cascade — so/foundationand/blogserved their ownog:imagebut the root's Twitter card. Every segment now has a re-export beside itsopengraph-image.generateMetadatadeclares anopenGraphobject withoutimages, which drops the inherited card. All 12 projects now get their own titled card.Incidental fixes
app/not-found.tsxstyled its linktext-green— green as an affordance colour, which the design system explicitly rules out. Rebuilt on the registry type scale.sitemap.tsadvertised the five dead/use-casesURLs and never listed/agentor/compute. Rewritten, with ecosystem projects added.package-lock.jsondeleted. Six deps had been added vianpmin a pnpm-pinned repo, leavingpnpm-lock.yamlstale —pnpm install --frozen-lockfilewas failing, which would have broken the Vercel build. Lockfile regenerated and verified.Verification
pnpm typecheck,pnpm lint(0 warnings) andpnpm build(130 static pages) all clean, against both the Notion register and the markdown fallback. Pages verified in light and dark at 390px, sm, md and wide desktop.Before this can ship
agentAppOriginis the console's Vercel preview (lib/site.ts) —livepeer-console.vercel.app, not its final address. It feeds the most prominent CTA on every page, so it still needs one edit before launch. The Agent playbooks link is the one product link left onlivepeer.peaceno.de: the console has no playbooks page, and a mockup beats a 404.forum.livepeer.org/or similar rather than a permalink.🤖 Generated with Claude Code