07 - Marketing and documentation

On this page 9

The public face: what reviewos.org shows a visitor, and the documentation someone reads when they decide to run it.

Landing page

  • Public source clone links use the canonical Git URL builder and its supported mount.

  • Design and build resources/views/index.stx using the stacks-design-taste skill

  • Nav and footer as components in resources/components/

  • Light and dark, following the skill's dark mode protocol

  • No em-dashes in any user-visible copy, per the house pre-flight rule

  • stx signals and composables only; no vanilla DOM access in templates

  • Iconify classes for icons, no icon packages and no hand-rolled SVG paths

  • Styling is a scoped <style> block over CSS custom properties rather than Crosswind utilities, because the page carries its own two-theme token set. Revisit if the marketing page and the application need to share a system.

  • Responsive down to a phone, with the hero legible without scrolling

Marketing site structure

Modelled on the uptimestatus marketing site: one catalog feeding the navigation, a page per feature, a page per use case, and comparisons that say plainly what the alternative does better.

  • resources/functions/marketing.ts as the single catalog of features, use cases, and comparisons, so a page and its menu entry cannot drift apart

  • resources/views/layouts/marketing.stx shared by every marketing page below the landing one

  • Features mega menu, grouped into Review, Repositories, Automation, and Operations, built from the catalog and present on the landing page too

  • A page per feature under /features/<slug>, twelve of them, each with its own argument rather than a restatement of the summary

  • Agentic usage as a first-class feature: /features/agentic-review, and a use case at /for/ai-assisted-teams, because review is the control that matters when agents write most of the diffs

  • Use cases mega menu and a page per audience under /for/<slug>

  • Comparison pages under /compare/<slug> for GitHub, GitLab, and Forgejo, each stating what the alternative genuinely does better

  • Index pages at /features, /for, and /compare

  • Footer linking every feature, use case, and comparison

  • Navigation reordered around the product rather than the argument: Discover promoted to the top bar on both the landing page and the shared marketing layout, Use cases and Compare demoted to the footer, which now links every page in both catalogs rather than the subset that fitted in a mega panel

  • Sign in and sign up in the top bar, on the marketing pages and inside the product. The landing page's only call to action was "self-host it", so somebody who wanted an account on this instance had to guess the URL; /login and /register both existed and nothing pointed at either. AccountNav replaces SettingsLink, which rendered nothing at all to a signed-out reader

  • Social sign-in on both auth pages, drawn from the framework's configuredSocialProviders() so a provider whose keys are missing never renders as a button that fails. GitHub, Google, Apple and the rest light up as their keys are set; see app/Actions/Auth/social.ts

  • Social sign-in honors the API mount prefix through both halves of the flow. The redirect and callback actions read the provider after the auth segment instead of assuming a root mount, and the callback accepts Apple's required form_post response as well as OAuth GET callbacks. That POST is narrowly exempt from the site's double-submit CSRF middleware because its one-time OAuth state is the protocol's own CSRF check; the state cookie uses SameSite=None; Secure for Apple so the browser can return it on a cross-site POST, while ordinary session cookies stay Lax

  • "Browse the source" points at this instance rather than at GitHub, along with the clone command, the nav, both footers, and the docs hero. A forge that sends its own readers to a competitor to read its own code has not made its case. The GitHub mirror stays linked and labelled

  • Featured repositories on the landing page: real repositories on this instance, with live star counts and languages, placed where the argument runs out. Curated in app/Actions/Explore/featured.ts rather than computed, because trending and recently-active both rank by accidents of the import on a freshly mirrored instance

  • Pricing page, if a hosted offering ever exists

    **Not applicable, and recorded rather than left open.** There is no hosted offering, and the
    condition on this box is the whole of it. A pricing page for software you self-host is a page
    about nothing; if a hosted offering is ever built, this box comes back with it.
    
  • Per-feature screenshots, once the review interface renders real data

    **`buddy screenshot`, on Bun's own webview.** `Bun.WebView` is in the runtime this project
    already runs on and it navigates, evaluates, resizes and screenshots - for a tool whose job is
    "open five pages and photograph them", downloading a headless Chrome would be the tail wagging
    the dog and one more thing to keep current in CI.
    
    Five shots are declared in `app/Actions/Screenshot/shots.ts` beside what each is *for*, so a
    reader knows what may break them. Every one names a selector that must match before the
    shutter opens - a tool that sleeps a second and fires produces a library of spinners - and a
    capture under 5KB is refused rather than written over a good picture.
    
    Taken against `reviewos/linux` #1, which is Linux `v6.0...v7.0`: the review shot is the actual
    interface on 80,610 files of kernel history, with the file tree, the hunks, the syntax colour
    and the mechanical-hunk labels in it. 2880x1800 each, the 1440x900 viewport at this display's
    density. `tests/unit/screenshots.test.ts` keeps them honest, because an image cannot fail a
    test by itself: every declared shot has a file, none is the thin blank a failed capture
    writes, and each is an exact multiple of its viewport.
    

Landing page

  • Screenshots of the actual review interface, once phase 4 exists. Placeholders now, replaced then; shipping invented screenshots of software that does not exist yet is not acceptable.

    Closed, and worth saying how: **this page has no placeholders to replace, because it never
    used any.** It shows live repositories from the instance and a real terminal - "Not
    screenshots. Every one of these is a repository on this instance" is already on the page,
    which is a stronger claim than a picture.
    
    So the pictures the pipeline produces go where a picture is the right medium: the
    documentation, a README, a press kit - places that cannot embed a live instance. The rule at
    the end of this box held throughout: nothing invented was shipped, and what exists now is a
    photograph of the software running against the Linux kernel.
    
  • Contrast measured rather than eyeballed: every text pair passes WCAG AA in both themes

  • Remove the leftover template assets in resources/assets/ (3.1 MB: eight national-park illustrations, eight fonts, a demo stylesheet and a demo script). Nothing referenced any of it but the two template blog posts, which went too - content/blog/ shipped "Introducing Stacks", and config/blog.ts was titled The Stacks Blog with stacksjs.com as its canonical URL, so a blog build here would have published another product's branding.

  • Open Graph and Twitter card metadata, with a generated social image. First shipped as one card, written as HTML and screenshotted through headless Chrome by buddy social:card - a Chrome dependency for one image, which is why there was one image.

  • A card per page, and a complete <head> on every page. config/images.ts declares one card per route and buddy generate:images draws all of them with ts-images, no browser in the pipeline; the copy for features, use cases and comparisons is derived from the catalog in resources/functions/marketing.ts so a card cannot quote a headline the page no longer has. resources/functions/meta.ts resolves the canonical URL, the card and the robots policy from the request path, and both layouts render the full set - description, canonical, og:* with the image's dimensions, twitter:card, and noindex on the personal and unbounded views. Every view now declares a title and a description, which a test enforces.

  • Cards for the pages a build cannot enumerate. A repository, a pull request, an issue and a profile get a card drawn on request by /api/og, cached on disk and keyed on what the card says, so a rename redraws it with nothing to invalidate. Everything is resolved as a stranger, so a private repository gets the generic site card and no confirmation that it exists.

  • A favicon, which there was none of: nothing declared one and public/ held no favicon.ico, so every tab showed the browser's blank-page glyph and every request for it was a 404 in the log. scripts/brand-mark.ts draws the mark once as a PNG for the cards and as public/favicon.svg for the tab, off the same numbers the navigation's CSS uses; buddy generate:images --app-icons resizes the PNG into favicon.ico, the apple-touch icon, and a site.webmanifest that names the application. The documentation site declares the same three, so the tab does not change identity at /docs.

  • Lighthouse pass: no layout shift, fonts preloaded, images sized

    Measured rather than assumed, in a browser against the served page. **Layout shift is zero**
    across repeated loads, with no shift entries recorded at all, and **there is nothing to size**:
    the page carries no images, because the icons are Iconify classes and the product shot is a
    terminal drawn in CSS. Two of the three are done and were done by construction.
    
    What is left is the fonts, and it is a decision rather than an oversight. The page loads Geist
    from `fonts.googleapis.com`, which is the one render-blocking third-party request on it (125ms
    cold, cached afterwards) and the one thing that sends every visitor's address to somebody else
    - on a product whose whole argument is that you host it yourself. Self-hosting the two woff2
    files removes the round trip, the privacy leak and the single point of failure in one go, and
    it is the only option here that does not put a `link` with an `onload` handler in the markup.
    Left open because it means vendoring font binaries, which is a choice about what this
    repository carries.
    
    The measuring turned up something better than any of it: **the server was not compressing
    anything.** 253 KB of HTML, `Accept-Encoding: gzip` ignored, on every page of every request.
    Fixed upstream in `@stacksjs/bun-router` 0.0.24 - see the note in
    [00 - Bootstrap](./00-bootstrap.md).
    

Documentation site

  • bunpress configured through config/docs.ts, served by ./buddy dev:docs

  • The built site actually served at /docs in production. It was not, and the symptom was misread for a while as "the roadmap page is empty": nothing served /docs at all, so every documentation URL fell through to [owner]/index.stx and rendered "There is no account called docs here" - a not-found wearing a profile page. resources/views/docs/ reads bunpress's output; the deploy builds it in preStart.

    Written first as a route with an action behind it, which is the shape the attachment and git
    routes use, and it never answered: this application resolves file-based stx views for
    everything outside `/api`, and the route registry belongs to the API process. Two further
    shapes lost to the view router's ranking before the working one - a `[page].stx` beside an
    `index.stx` loses to `[owner]/[repository]/index.stx`, and a `[page]/index.stx` wins. Social
    sign-in moved under `/api/auth/...` for the same reason.
    
  • Phases 14 and 15 in the docs sidebar. Both existed as files, were named in the roadmap's own table, and were built to HTML; neither was in config/docs.ts, so the two largest phases were reachable only from inside another page.

  • .md cross-links resolve. The roadmap files link to each other as ./00-bootstrap.md, which is correct in the repository and on GitHub, and both names answer with the extensionless one canonical.

    **The second half of this used to say bunpress "does not rewrite" those destinations, and
    that stopped being true.** bunpress#86 landed source-relative link resolution in 0.2.22: on
    0.2.28 a `[guide](./guide.md)` is emitted as `href="/guide"`, verified on a fixture. It also
    rejects a destination that resolves to nothing, which is how three dead `./09-ci-cd.md`
    links in `12-api-and-agents.md` were finally caught after sitting on the published site.
    
  • Roadmap published as task lists that render as checkboxes

  • Roadmap index counts and "not started" states are checked by tests/unit/roadmap.test.ts, so completing work cannot leave the summary silently stale

  • Getting started: requirements, install, first repository, and what a push actually does

  • Configuration reference: every environment variable that matters, and what it does. docs/configuration.md is generated from .env.example and the source that reads each variable, so it says where a value is used, which ones the boot check validates, and which ones git sets for a hook rather than you. The hand-written table it replaced still named Meilisearch months after the move to Typesense, which is the argument for generating it.

  • Architecture: how a request becomes a git operation, where repositories live on disk, and the two planes checks run in

  • API reference for the JSON API, generated from the actions rather than written by hand. buddy docs:reference writes docs/api.md from the OpenAPI document and docs/webhooks.md from the payload module; --check and tests/unit/docs-reference.test.ts fail when the committed copies have drifted, because a generator nobody runs is a hand-written page with extra steps.

  • Webhook payload reference, every event from WEBHOOK_EVENTS with the envelope described once

  • Declare the remaining endpoints' inputs on their actions. 48 of 157 operations carried a validations block; the rest validated inside the handler, so the document had nothing to publish for them and the page named the action instead of guessing.

    **Done: 186 actions declare their inputs, and the 16 that do not have none to declare.**
    Those sixteen are the static documents (`jwks.json`, the OpenID configuration, the OpenAPI
    document itself, the step keys, the atproto client metadata), the endpoints that take only a
    credential (`whoami`, logout, the token list, the review queue, metrics), and the ones driven
    by headers and a body stream rather than named fields (the runner's claim, cache and artifact
    upload, attachment serving, the mirror webhook). An empty block on those would be noise
    pretending to be documentation.
    
    Two things made this more than a mechanical sweep. Keys read *through a helper* are invisible
    to a scan of the action: `authorizeRepository` reads `owner` and `repo`, `browseContext` adds
    `ref`, and the pull helpers add `number` - so an action whose body never says `request.get`
    still has four inputs a caller must send, and those are declared. And nothing is marked
    required: this describes what an endpoint reads, and turning it into enforcement across
    eighty-seven endpoints in one commit would be changing what they accept while claiming to
    document it.
    
    Verified by exercising the endpoints rather than by the count: search, releases, explore,
    health and repository code search all still answer 200, and the topics endpoint - the first
    one converted, as a probe before the rest - publishes `owner`, `repo` and `topics` in the
    document where it previously published nothing.
    
    What is left is a refinement rather than a gap: a boolean-ish field such as
    `delete_branch_on_merge` is declared as a string, which is what a form actually sends. Typing
    those precisely is worth doing per endpoint, with the coercion checked, rather than guessed at
    in bulk.
    
  • Contributing guide: the model to migration to action to route to view order, and the expectation that roadmap boxes are ticked in the same commit as the work

  • Search across the docs. The overlay had been rendering, taking a query, and answering nothing: it fetches /search-index.json, which bunpress serves from 0.2.0 on, and the framework pinned ^0.1.18. Bumped upstream, released as Stacks 0.70.370, and verified here - a query returns hits across the guides, the roadmap, and the generated API reference.

Self-hosting guide

  • Requirements and sizing, with honest numbers rather than optimistic ones. Measured on a real instance: boot to serving, resident memory idle and after a hundred requests, ref advertisement and a full clone of a 190 MiB pack.
  • Install with pantry, and install with Docker. Both paths, with the systemd units for the non-Docker one and why TimeoutStopSec has to be above the drain window.
  • Reverse proxy and TLS, including the x-forwarded-proto that decides whether the session cookie is marked Secure - getting it wrong is a login that appears to work and returns you signed out.
  • SMTP configuration for notifications, starting with the fact that the default driver is log and sends nothing, which in production is a password reset nobody receives.
  • Backup and restore, covering both Postgres and the repository directory. Documented after restoring from it against a copy, not merely written.
  • Upgrading, including migrations, and which failure is the one where the answer is "restore" rather than "fix and re-run"
  • Hardening checklist: what an instance should look like before other people depend on it, every line of it covered elsewhere on the page

Brand

  • Wordmark and icon

    `public/wordmark.svg`: the mark and the name in one lockup, at the spacing the nav already
    uses, with the icon alone remaining `favicon.svg` - the same two bars in the same square. One
    file rather than two because a wordmark and its icon drifting apart is how a README ends up
    with last year's logo beside this year's, and drawn as paths rather than set in Geist so it
    renders identically anywhere it is embedded: a README on another forge, a slide, a badge,
    none of which will have the typeface.
    
  • Favicon set

    Thirteen files under `public/`: the SVG that modern browsers prefer, the `.ico` for the tab
    and for every crawler that still asks for `/favicon.ico`, the apple-touch icon, nine PNG
    sizes from 16 to 512, and the `site.webmanifest` that names them. Declared by the marketing
    page and by the documentation site, so the tab does not change identity at `/docs`.
    
  • Colour and type scale recorded so the marketing page and the application agree. docs/design.md is generated from the three stylesheets that declare the palette, and tests/unit/design-tokens.test.ts fails when a token two of them share stops having the same value - which is how two copies of a palette drift, one hex at a time, with nothing failing because both files are valid CSS.

  • Social preview image

    Two halves, because a forge needs both. `public/images/social.png` is the static card for the
    marketing pages, and `/api/og` draws one on demand for the things that actually get shared and
    cannot be enumerated at build time - a repository, a pull request, an issue, a profile. The
    page declares `og:image` with its type, width, height and alt text, because a card missing its
    dimensions renders as a broken square on the platforms that will not fetch it to find out.
    
  • The landing page advertised progress figures that disagreed with the roadmap. It showed 06 Search and explore - 20/21 and 07 Marketing and docs - 21/45 while docs/todo/index.md had both complete, at 21/21 and 58/58, and the prose beneath still called search, discovery and this site "the current open tracks". A public page underselling finished work is the one direction of drift nobody reports, because it reads as modesty.

    The list was hand-written under a comment saying "Counts mirror docs/todo/index.md", which is
    the same arrangement that had already drifted for the Typesense port and for AGENTS.md's
    search engine. So `app/Docs/roadmap.ts` reads the table instead - the move
    `app/Docs/configuration.ts` already makes for `.env.example`. The core path is always shown
    because the page's argument rests on it; phases after it appear only while unfinished, so the
    section can neither claim the work is done while a phase is open nor list eleven finished
    phases to say so. It renders 00 to 07 as Done, then `09 Checks and CI 121/122` and
    `15 Pipelines 188/189`.
    
    **Two things worth recording from doing it.** The first attempt put the derivation in the
    view as `[...core, ...open].map(...)`, and that value did not reach the template at all:
    `@foreach` reported "phases is not iterable" into an HTML comment, which is the silent
    failure AGENTS.md warns about and which nobody reads. `STX_DEBUG=1` named it in one line. A
    single awaited call does arrive - the shape `featured()` already used ten lines above it - so
    the selection moved into `shapeLandingPhases` and the view is one line. The precise cause was
    not isolated beyond "that form did not carry, this one does".
    
    The second: the first test asserted the page's counts matched the roadmap's by comparing
    `landingPhases()` with `roadmapPhases()`. Both read the same file, so they cannot disagree -
    it passed with the roadmap deliberately mutated to 100/122. The shaping is tested against a
    fixture now, verified by mutating the fixture and watching it fail. That is the second
    test-that-cannot-fail written in this codebase today, and both had the same shape: asserting a
    thing against itself.
    
  • The crawler policy moved out of a static file, which had been quietly contradicting the sitemap. public/robots.txt was hand-written, and Stacks serves a file in public/ in preference to its own generated one. A static file cannot know the origin it is served from, so it named no Sitemap: at all - the framework warned about exactly that on every boot, and its advice was to delete the file.

    Deleting it would have been a regression. The generated /robots.txt carried one rule, /api/, and none of the rules that were added after a crawler walked /tree/ across every branch and tag of every mirrored repository until the page process hit its memory ceiling and the site stopped answering, twice. So the policy moved into config/app.ts under seo.robots first, verified by rendering what the framework would serve, and the file went afterwards.

    That turned up a second problem nobody had reported. Both generators read the same config, so /sitemap.xml now excludes what /robots.txt disallows - and it had been listing thirteen URLs it simultaneously told crawlers not to fetch: /login, /register, /search, /notifications, /reviews, /new, /forgot-password, /unsubscribed and the five /settings/* pages. That is the contradiction Search Console reports as "Submitted URL blocked by robots.txt", and no static file could have fixed it, because the sitemap generator was never reading the static one. 28 pages are listed now, all of them pages the site exists to be found by.

    tests/unit/robots-txt.test.ts now tests the generated output rather than a file in public/, which nothing serves any more. Its last assertion was also inverted: it read does not point at a sitemap that is not there, and the sitemap has always been there - public/sitemap.xml is absent, so the generated one serves. The test had pinned a static file's limitation in place as though it were the policy.

  • Audit outbound email and SMS triggers, defaults, holds, skips and inactive scaffold paths in docs/messages.md, with BunPress email previews and explicit inbox-only and SMS limitations.

  • Correct the self-hosting search port and failure behavior, link all scale-out roadmap phases in the sidebar, and require BunPress message-preview support in the source Stacks docs package. Harden preview links in BunPress itself.

  • Avoid claiming configuration variables with no direct application reader are necessarily framework-only.

The interface, measured rather than eyeballed

The box above that says "every text pair passes WCAG AA in both themes" was true of the palette and not of the pages. The tokens have always passed - --muted on --surface is 5.5:1 in light and 7.3:1 in dark, and a unit test said so. What nobody had measured was what the browser computes after a stylesheet is done with those tokens: a name that no palette declares, a fill that drops out as invalid, an opacity on a row that multiplies through to the muted text inside it. Every text node on twenty-nine pages, in both themes, was read out of a real headless Chrome and scored against its own composited backdrop.

  • The primary button on three screens was invisible, in both themes. labels, milestones and the issue list each drew it as color: var(--bg) on background: var(--fg), and --fg is not a token any palette here declares. An undeclared custom property is not a parse error: the declaration is dropped at computed-value time, the property falls back to inherited or initial, and the page renders. So the fill never applied and the label rendered in the page background colour on the page background - 1.04:1 in light, 1.08:1 in dark, measured. "Create label", "Create milestone" and "New issue" are the one control each of those screens is built around. The buttons come from the layout now, which has had a correct .btn-primary the whole time, and .btn-danger moved there too so the three pages that invented one against --danger or --del - also undeclared, also falling back to a hardcoded red at 3.2:1 on the dark surface - can stop.
  • A ticked task box in rendered Markdown looked unticked, for the same reason and with the same two tokens: the fill was --fg and the tick was cut out of it in --bg.
  • The run log's ANSI colours were half a palette. The comment above them said "named rather than hardcoded, so a theme decides what red is on the background the reader actually has", and four of the eight were hex literals chosen against somebody else's terminal: black at 2.4:1, magenta at 3.4:1 in light, cyan at 3.6:1 in dark. They are --ansi-gray, --ansi-blue, --ansi-magenta and --ansi-cyan now, one value per theme, and blue has its own rather than borrowing the accent, which cyan already is in dark.
  • Six states were dimmed with opacity and opacity does not know what is underneath it. A read inbox row, a draft in the review queue, a resolved thread, a file already reviewed, one whose tick cannot be verified, and the end of a paginated list. Each one dimmed a whole container, so the --muted metadata inside it dimmed too: 2.3:1 for a read row, 2.5:1 for a draft, 2.1:1 for the pager. All of them say the same thing with colour instead - --muted has a floor under it, and the structural signals that were already there (the unread marker on the left edge, the strike-through, the word "resolved") carry the rest.
  • Twenty-five more references to tokens that do not exist, found by scanning every var() in resources/ against every declaration: --border, --font-mono, --panel, --panel-2, --surface-2, --surface-strong, --bg-subtle, --r-field, --add, --del, --danger, --warning. Most carried a fallback and so were merely a second vocabulary nobody maintained; --border carried none, so five borders computed to currentColor and drew at full text strength.
  • The light/dark choice now reaches every page. It was honoured by the application layout and ignored by the marketing layout and the landing page, which answered only to the operating system - so choosing light in settings and then clicking the logo took you from a light forge to a dark front door. All three documents read the same key now. The landing page and the marketing layout load it from public/theme.js rather than inline, because stx compiles an inline <script> in a template into a scoped client block and hands it to its runtime, and those pages do not load the runtime: written inline it was compiled and then silently dropped.
  • The palette applied at all without scripting. Every dark selector was :root[data-theme='…'], and the attribute is set by a script - so with scripting off nothing matched and a reader on a dark system got the light palette. :not([data-theme]) is in each list now.
  • tests/unit/interface-contrast.test.ts holds the floor: every pair of the six palettes the site ships, scored out of the stylesheets, plus the check that would have caught all of the above - every bare var() in resources/ names a property something declares.
  • The drift check in tests/unit/design-tokens.test.ts was reading less than it looked like it was. Its parser counted a palette only under a bare :root inside a prefers-color-scheme query, so the application layout's dark palette - declared twice, both times under a data-theme selector - was never compared to anything, and a selector list written one selector per line was read from its last line only. Thirteen dark tokens were being checked across two files; it is fifteen across six declarations now, --ok and --ok-bg among them.

The documentation site

  • The search control on /docs was the least readable thing on the page. Its label measured 2.87:1 in light and 3.37:1 in dark, and its border 1.29:1, because bunpress drew both from tokens that had never been measured - --bp-c-text-3, which it also uses for code block language tags and line numbers, and --bp-c-divider, the content hairline, which every control in the theme had borrowed because --bp-c-border was declared and never used. Fixed upstream in bunpress 0.2.31 (stacksjs/bunpress@e96789f) along with nine more: the dark filled button at 2.02:1, the bun theme's links at 2.05:1, GitHub Light's comment and keyword tokens on a non-white code block, a gradient heading no checker can see, focus-mode dimming, and a 404 page whose only link had no colour of its own.
  • bunpress applied the reader's light/dark choice after the document had parsed, from a script at the end of the body - so every page load started light and turned, and with scripting off there was no dark palette at all. The stored choice is resolved in the head now and the dark rules are mirrored under prefers-color-scheme, generated from the rules themselves rather than written out beside them. Same two faults this repository had in its own layouts, found the same way.
  • syntaxHighlightTheme was github-dark while the docs render in both. The highlighter resolves one theme at build time and writes it inline on every token, and bunpress corrects it for dark mode with !important - a correction that only runs from a light theme toward a dark one. So the dark half worked by accident and the light half had GitHub Dark's palette on a #f6f6f7 block: string literals at 1.42:1 on every code sample in the documentation. It is github-light, and bunpress supplies the dark half.

Naming

  • "Explore" and "Discover" sat next to each other in the navigation bar. They are synonyms in English and two different things in the product, so the only way to learn which held repositories and which held events was to click both. They are Repositories and Activity now, named for what is on them. The paths stay /explore and /discover: links to them are already out there, and a URL is not a label.