# Public project pages

> Every public project gets a page on its workspace subdomain, with builds, downloads, channels, versions, and declared access anyone can read.

Canonical: https://docs.extension.dev/share/public-pages
This page as Markdown: https://docs.extension.dev/share/public-pages.md
Section: Share and preview
Siblings: [Build links and channel links](https://docs.extension.dev/share/build-and-channel-links.md) · [Share an unpublished extension build for review](https://docs.extension.dev/share/unpublished-build-for-review.md) · [Manage shares](https://docs.extension.dev/share/manage-shares.md)
Map of every page: https://docs.extension.dev/llms.txt · Whole corpus: https://docs.extension.dev/llms-full.txt

---

# Public project pages

A public project page is a build page anyone can open without a token or an
account. Reach for it when a build should be findable by everyone who holds
the link, not by one reviewer.

## Quick start

Set the project's visibility to public in project settings:

```
console.extension.dev/<workspace>/<project>/project-settings/visibility
```

The project then answers at `<workspace>.extension.dev/<project>`, and every
build, channel and version under it gets an address of its own.


## How it works

The workspace lives in the host, not in a path segment, so a link carries the
workspace identity before the page loads. Under the project root every page
is a noun: a build, a browser's artifact, a channel, a version.

The per-browser build page is where strangers land. It offers the artifact as
a zip, the executable downloads where the build produced them, and an
**Access** tab titled **What this build asks for**. That tab lists each host
pattern, permission and optional permission the manifest declares, with a
line on what it unlocks.

The zip streams straight from the registry at
`registry.extension.land/<workspace>/<project>/builds/<buildId>/<browser>.zip`
as bytes with `Content-Type: application/zip` and no redirect, so a plain
`curl` receives it. A public build's zip is cached as immutable for a year.

A project is either `public` or `private`, and the platform serves this page
only for a project declared `public`. A project nobody declared is private.
Two gates read the same declaration. The page gate hides the listing, the
name, the icon and the changelog behind a card that names nothing. The
registry gate answers `401` for the build bytes whether the file exists or
not. A deny always wins, so a project declared `private` is refused even if
an older allow entry is still around. A token scoped to the project opens
both gates, so an owner's view keeps working while a stranger is turned
away.

Making a project public exposes its listing and its builds. It does not
expose build logs, store credentials or environment variables. Those never
leave the console.

Visibility is a setting of the extension.dev project, not of its GitHub
repository. Workspace owners and admins change it. Everyone else reads it.
Changing it sets every channel's audience to match. A project connected through the console's onboard page starts with the
visibility of its repository. A project created on `/import` starts private
whatever its repository is. The platform re-declares the visibility to the
registry on every completed build, so the declaration heals itself. The
registry refreshes its lists about once a minute, so a change is not instant
in either direction. Nothing recalls a file someone already downloaded.

Public does not mean indexed. The workspace subdomain refuses every search
crawler in `robots.txt` and again with a `noindex, nofollow` tag on the page,
so a public build page is reached by a link you send and not by a query
someone types. Link preview bots are admitted by name, so a shared link
unfurls where it lands: Twitterbot, facebookexternalhit, Slackbot,
Slackbot-LinkExpanding, LinkedInBot, Discordbot, TelegramBot, WhatsApp,
redditbot, Embedly, SkypeUriPreview, Mastodon and Bluesky. A private project
unfurls to a card that names nothing, the same gate as the page.

A private project has no public project page. It shares one build with a
build link, or a public channel with a channel link. See
[Build links and channel links](/share/build-and-channel-links). A member
mints a token link with this call:

```
POST /api/projects/{id}/share
```

The call needs `buildSha`, or a `scope` that names a build, a channel or the
whole project, and takes `ttlHours`. It returns a `shareUrl` carrying a
`?share=` token, with its expiry and its `scope`. Any workspace member may
call it. It refuses a public project with `400`, because a public project
already has a page.

## Reference

| Path under the project root | Contents |
| --- | --- |
| `/builds/<buildId>` | One build, every browser it produced |
| `/builds/<buildId>/whats-new` | What changed in that build |
| `/builds/<buildId>/<browser>` | One artifact: zip download, executable downloads |
| `/builds/<buildId>/<browser>/access` | What this build asks for |
| `/channels/<channel>` | What the channel currently holds |
| `/<channel>` | Shortcut to the channel page |
| `/versions/<version>` | A version across the builds that carried it |

| Gate | What it protects | What a stranger gets |
| --- | --- | --- |
| The page | The listing, the name, the icon, the changelog | A card that names nothing |
| The registry | The build bytes | `401`, whether the file exists or not |

| Share call | Value |
| --- | --- |
| `ttlHours` | 7 days (168 hours) by default, from 1 hour up to the plan's share window: 30 days on Free, 365 days on Pro |
| `buildSha` | Points the link at `/builds/<sha>` and opens only that build |
| `scope` | `{kind: "build", name: "<sha>"}`, `{kind: "channel", name: "<channel>"}` or `{kind: "project"}`. Without `scope` or `buildSha`, `400` `SCOPE_REQUIRED` |
| Public project | `400` "Project is public; share links are unnecessary." |
| Non-member | `403` |

## When it fails

- **The page shows a brand card instead of your project**: visibility is not public. That is the gate working. Set it under **Visibility** in project settings.
- **`POST /api/projects/{id}/share` answers `400`**: the project is public. Link the page itself.
- **A channel shortcut says the link does not resolve**: the name is not a promoted channel of this project, or the link is out of date. Promote a build to that channel, or use the full `/channels/<channel>` form.
- **A share link says it has expired**: a share token cannot be renewed. Mint a new one, or send a [channel link](/share/build-and-channel-links), which never expires.
- **The page never appears in a search engine**: it is not meant to. Send the address, or paste it somewhere a link preview renders it.

## Next

- [Build links and channel links](/share/build-and-channel-links) for which link to send
- [Share an unpublished build for review](/share/unpublished-build-for-review) for one reviewer instead of everyone
- [Releases](/builds/releases) for how a build reaches a channel
- [Publishing](/publish/overview) for the store lanes
