# Website

The project site lives in [`web/`](https://github.com/shaharia-lab/slackcli/blob/main/web) and publishes to GitHub Pages at
**https://slackcli.dev**. It is an Astro site with three parts: a hand-laid
landing page, the documentation you are reading now, and a blog.

The site sits at the root of its own domain, so no route carries a path prefix.
The domain, its DNS and the Pages custom-domain setting are all managed in
`shaharia-lab/infrastructure`, not here, and there is deliberately no `CNAME`
file in this repository: `github_repository_pages.cname` is what writes the one
GitHub serves.

## Run it locally

```bash
cd web
npm ci
npm run dev            # http://localhost:4321/
```

```bash
npm run build          # clean, sync docs, resolve the release, build to web/dist
npm run preview        # serve web/dist
npm run check:links    # every internal link and fragment in web/dist
npm run build:og       # redraw public/og.png (needs a local Chrome)
```

The site uses **npm**, not Bun, even though the rest of the repository is a Bun
project. It has its own `package.json`, its own lockfile and its own
`node_modules` under `web/`, none of which the CLI build ever sees. Astro and
Starlight are best exercised on npm, and the sibling site at
[myagento.app](https://myagento.app) is built the same way, so the two stay
diffable.

`web/.npmrc` pins the `@shaharia-lab` scope to the public npm registry. Do not
remove it: a developer whose own `~/.npmrc` maps that scope to GitHub Packages
otherwise gets a 404 on the design system that reads as "this package does not
exist".

## The docs half is generated

**Never edit anything under `web/src/content/docs/`.** It is generated from this
`docs/` directory by `web/scripts/sync-docs.mjs` on every build, and your edit is
overwritten by the next one. Edit the file in `docs/` instead.

The sync script exists because the markdown here is written to be read on
GitHub: relative `*.md` links, no frontmatter, headings that serve as anchors.
At build time it lifts the H1 into the frontmatter `title`, derives a `<meta>`
description from the lead paragraph, and rewrites every relative link into a
site route, resolved against the directory of the file that wrote it.

### Adding a documentation page

Two steps, and the build fails loudly if you do only the first:

1. Add the file under `docs/user-guide/` or `docs/development/`.
2. Add it to `PAGES` in [`web/docs.manifest.mjs`](https://github.com/shaharia-lab/slackcli/blob/main/web/docs.manifest.mjs),
   with its `slug`, its sidebar `label` and its `group`.

The manifest is the single source for both the generated pages and the sidebar,
so the two cannot disagree. A file in `docs/` that is not listed fails the build
rather than silently never publishing, which matters more than it sounds: an
unlisted page also turns every existing link to it into a GitHub blob URL
instead of an internal one.

Give the page a `description` in the manifest if it opens with anything that is
not a sentence, such as a code fence, a table or a bullet list.

The `slug` mirrors the path with the extension dropped, and a directory's
`README.md` collapses to the directory itself:

| File | Route |
|---|---|
| `docs/README.md` | `/docs/` |
| `docs/user-guide/README.md` | `/docs/user-guide/` |
| `docs/user-guide/search.md` | `/docs/user-guide/search/` |

### Renaming a heading is a breaking change

Pages cross-link by anchor, and a renamed heading breaks those silently: the
markdown still renders and the link just lands nowhere. `npm run check:links`
catches it, and the `Site check` workflow runs it on every pull request that
touches `docs/` or `web/`.

## Writing a blog post

Add a markdown file to `web/src/content/blog/`. The frontmatter schema is in
`web/src/content.config.ts`:

```yaml
---
title: A sentence that is not a label
description: One sentence. It is the meta description and the RSS summary.
date: 2026-08-14
author: Shaharia Azam
tags: ['Design']
featured: false      # at most one post may set this
draft: false
---
```

Links from a post to a documentation page are written as absolute site paths:
`/docs/user-guide/scripting/`. Markdown cannot call the site's `url()` helper,
so a post is the one place a route is spelled out by hand, and
`npm run check:links` is what stops those from rotting.

## Where things are

| Path | What it is |
|---|---|
| `web/site.config.mjs` | The address, the repo, the share card, the analytics and consent snippets |
| `web/docs.manifest.mjs` | Which docs publish, in what order, under what heading |
| `web/faq.config.mjs` | The landing FAQ, rendered both as markup and as `FAQPage` structured data |
| `web/astro.config.mjs` | Astro, Starlight, the sidebar, and the site-kit options (theme, Star button, Edit on GitHub map) |
| `web/src/pages/index.astro` | The landing page |
| `web/src/components/Term.astro` | The replayed terminal session in the hero |
| `web/src/components/Install.astro` | The install section, driven by the release API |
| `web/src/styles/` | `site.css` (landing and blog) and `consent.css` (cookie banner); the theme itself comes from site-kit |
| `web/scripts/` | `sync-docs`, `fetch-release`, `clean`, `check-links`, `render-og` |
| `web/design/og-image.html` | Source for `public/og.png`, rendered by hand |

## Design system and site-kit

The look and the docs chrome come from
[`@shaharia-lab/site-kit`](https://github.com/shaharia-lab/site-kit)
([docs](https://shaharia-lab.github.io/site-kit/docs/)), the layer shared by
every Shaharia Lab open-source project site. `web/astro.config.mjs` passes it to
Starlight as `plugins: siteKit({ ... })`:

- **Theme.** The `agento-code` theme with the `forest` accent: colour, type,
  spacing, border and motion tokens from
  [`@shaharia-lab/agento-code`](https://www.npmjs.com/package/@shaharia-lab/agento-code),
  self-hosted fonts, the Starlight surfaces and the code-block colours, in light
  and dark. `web/src/layouts/Page.astro` imports the same theme files, so the
  landing page and blog share one definition with the docs.
- **Page actions** under every docs title: Copy page, Open in ChatGPT, Open in
  Claude, View as Markdown and Edit on GitHub.
- **A Markdown twin** of every docs page at its URL plus `.md`, and
  `/llms.txt`, `/llms-full.txt`, `/llms-small.txt`.
- **Edit on GitHub** is mapped from `web/docs.manifest.mjs` to the file in
  `docs/`, because the generated copy under `web/src/content/docs/` is
  gitignored and would 404.
- **The Star button** in the docs header, with the count from
  `web/src/data/repo.json`.

A change to the look or the docs chrome belongs in site-kit, not here: it lands
once and reaches every site with a version bump. Only site-specific CSS lives in
`web/src/styles/`.

Read a colour through a token, never as a literal. The one deliberate exception
here is `web/design/og-image.html`, which is rendered standalone with no
stylesheet behind it.

## Analytics

There is none unless the build is given one. `PUBLIC_GTM_ID` is a repository
variable read by `gtmId()` in `web/site.config.mjs`; when it is unset the build
emits no third-party script and no cookie banner at all. Only the `Site`
workflow passes it, which is what keeps the tag off `npm run dev`, off fork
builds and off pull-request builds.

When it is set, Google Consent Mode v2 denies everything except
`security_storage` before the container loads, globally rather than for the EEA
alone, and the banner's Accept and Reject are deliberately the same control.

## What CI does

| Workflow | When | What |
|---|---|---|
| `Site check` | pull requests touching `docs/`, `web/` or `package.json` | Builds the site and checks every internal link and fragment |
| `Site` | pushes to `main` on the same paths, every published release, and manual `workflow_dispatch` | Builds and deploys to GitHub Pages |

`Site` also runs on a release because the landing page bakes in the current
version, the per-platform binary sizes and the checksums URL. Without that
trigger the site would advertise the previous release until somebody happened to
push a docs change.

## The domain

`slackcli.dev` is an apex domain served by GitHub Pages, wired in
`shaharia-lab/infrastructure`:

- `terraform/cloudflare` holds the zone and eight apex records, four `A` and
  four `AAAA` pointing at GitHub's published Pages addresses. Every one is
  `proxied = false` on purpose. A proxied record hides the request behind
  Cloudflare's edge, GitHub's ACME challenge never reaches Pages, the
  certificate is never issued, and "Enforce HTTPS" stays greyed out.
- `terraform/github-shaharia-lab` holds `github_repository_pages.slackcli` with
  `build_type = "workflow"` and `cname = "slackcli.dev"`. `build_type` is not
  optional: with the default the API expects a branch naming a source, and the
  deploy job fails at `actions/deploy-pages` with a Pages-not-configured error.

Two things follow from `.dev` being on the HSTS preload list. The site is
unreachable over plain HTTP, so it does not work at all until GitHub has issued
the certificate, and GitHub cannot issue one until the apex records resolve. The
old `shaharia-lab.github.io/slackcli` address keeps working throughout; GitHub
redirects it to the custom domain.