Get Started

Change the website, add a page to it, or translate it. www.local.link is built from the docs of every folder and holds no words of its own, so a page says the same on GitHub and on the website.

The site is link-web, a Next.js app on Vercel. It uses Next.js 16 with the App Router in src/app, React 19, TypeScript, Tailwind CSS 4 and ESLint, and needs Node 20.9 or newer. It doesn't run on the relay droplet, and the relay knows nothing about it.

One set of docs

These rules keep the docs one set, and cargo test in link-core checks them.

  • Every fact is in a folder's docs/en/, and every file there is a page here. Nothing is said only in the repository or only on the website.
  • A folder's pages are in that folder, changed in the pull request that changes its code. link-openclaw/docs/en/harnesses/openclaw/ is /harnesses/openclaw, and the core's link-core/docs/en/agents/ is /agents.
  • A page's path is its URL, and its place in the tree. A page's path in its folder's docs/en/ is its URL, whichever folder holds it, and the build fails if two folders hold one path. A folder puts its pages where they are in the site's tree, as link-harness does in docs/en/harnesses/link/, and an old address of a page that moved redirects to it.
  • A README.md outside docs/ holds no information. It has the name, the page on www.local.link, and links into the docs. GitHub shows one where the website has no page. So do .github/'s SECURITY.md, SUPPORT.md and CODE_OF_CONDUCT.md, which point at their pages.
  • Links between folders go to www.local.link, as in https://www.local.link/agents. That works on GitHub and wherever a folder goes, and the website turns it into a link within the site, in the reader's language. Inside a folder, links are relative.

The check covers every folder's links and anchors, that no link leaves its folder, and that no README.md outside a docs/ folder, nor a page in .github/, holds more than a hub.

Develop

cd link-web
npm install
# then http://localhost:3000
npm run dev

To show it to someone else, expose it through Link itself with lnk tunnel open 3000 --name site --public.

Before pushing, run the commands below. CI runs them in .github/workflows/website.yml on release pull requests that change any folder's docs/ or link-web/.

npm run lint && npm run build

Add a page

Add a Markdown file to the docs/en/ of the folder it's about, and its path there is its URL:

# www.local.link/pricing
$EDITOR ../link-core/docs/en/pricing.md
# or as a folder: the same URL
$EDITOR ../link-core/docs/en/pricing/README.md

Its first # heading is its title ("Local Link — Pricing"), and its first paragraph is its description in search results and link previews. Run npm run dev and refresh to see a change to the docs.

Every page is somewhere in the site's tree, which is the nested lists under ## Tabs in docs/en/website.md. A page outside it fails the build, and its URL is its place in the tree. Nest a line under the page it belongs under, ten at most to a page, with its page's # title as its text. A line at the top level is a tab:

- [Harnesses](https://www.local.link/harnesses)
  - [Link Harness](https://www.local.link/harnesses/link)
  - [OpenClaw](https://www.local.link/harnesses/openclaw)

A page with pages under it is a section. Its line names the section (OpenClaw), and its own page, its folder's README.md, is the first page under it, called by its title (Get Started). A folder's other pages go under its README by themselves, after a line, unless they're nested somewhere else. A part's specs come first, then its Decisions and Security. Every other line says exactly what its page's title says, and the build fails where one doesn't. A page that moves gets a line under ## Moved, with its old address and its new page, and the old address redirects to it.

docs/en/website.md also holds the site's name and description, the interface's own words (the copy button's) and, under ## Folders, the folders whose docs the site reads. A new folder is a line there.

The tabs are pills across the top, and on a wide screen each drops down its pages. Beside every page is the whole site's tree, the home page's too, the same from tab to tab, with the page you're on dark, the sections above it in bold, its ## headings under it and the other sections folded. Until the screen is wide, the tree folds into a bar under the tabs that says where you are and opens in place. Above its title, a page says the sections it's in, and below its cards come the pages before and after it in its tab, each a card on one line, as wide as its words. The tabs and the column are centred on any screen, the home page laid out as every other, and there is no footer: every page is in the tabs.

No heading has more than ten headings under it, so group a longer list under headings one level deeper.

The docs are plain Markdown, so they read the same on GitHub. A few things mean something more on the website:

In the MarkdownOn the website
The first # headingThe page's title, or its tab's name if it's a tab
Each ## headingA line under the page in the tree
The first paragraphThe page's description
---A break between cards
[Join Waitlist](https://... "button") alone in a paragraphThe big pill button
A list of links alone between two ---A row of pills
A sh code blockCommands with a copy button; other blocks are code
<!-- ... -->Hidden in both
A relative link to a .md fileA link to its page, anchor kept
A link to a page on https://www.local.linkA link to that page, in the reader's language
A relative link outside the docsThe file on GitHub, marked as leaving the site
A link to another siteA link with an arrow after it, so you know you're leaving

English is served without a prefix. /agents is app/[lang]/[[...path]] with lang = en, and /en/agents redirects to /agents in next.config.ts. Every page is built once, at npm run build.

Translate the docs

You write the docs in English, in each folder's docs/en/. A model writes the other languages beside them, in docs/<language>/, and never by hand. Each page names the English it was made from, and the website marks a page whose English has changed since. A language is one this folder's docs/ has a website.md for, whose ## URLs lists the folder and file names in that language, such as /es/agentes. The language pill in the tabs goes to the same page in another language.

cd link-web
# what's missing or behind in each folder's docs/es/
node scripts/translate.mjs plan
# send it as one batch, see its progress, and once it's done write docs/es/
export OPENAI_API_KEY=...
node scripts/translate.mjs submit
node scripts/translate.mjs status
node scripts/translate.mjs collect

collect writes only pages whose headings and code blocks match their English, and cargo test in link-core checks the same. A batch finishes within a day. With the repository's OPENAI_API_KEY secret set, .github/workflows/translate.yml does this by itself. A push to dev that changes any folder's docs/en/ sends a batch, and the finished one is pushed to the branch translate/es. A person opens it as a pull request into dev, because no workflow opens or approves pull requests. LNK_TRANSLATE_MODEL picks the model. The Terms of Service stay in English, which binds.

Deploy (Vercel)

The website goes live with a release. A merge into main releases lnk when it bumps the version (release.yml). The release's last step, once it is published, calls a Vercel deploy hook, so the site shows what the release does. vercel.json turns off Vercel's own deploys of main, and branches and pull requests still get preview URLs. A release that fails deploys nothing, and a change to the docs alone waits for the next release.

One-time setup:

  1. In Vercel: Add New → Project, import the repository.
  2. Set Root Directory to link-web. vercel.json pins the Next.js framework preset, so the default build settings are right as they are. Without the Root Directory, a project imported from the repo root is detected as "Other" and fails with No Output Directory named "public" found. The build reads every folder's docs/, outside the Root Directory, so keep "Include files outside the root directory in the Build Step" on, as it is by default.
  3. In Settings → Git → Deploy Hooks, make a hook for main, and save its URL as the VERCEL_DEPLOY_HOOK secret of the repository's release environment. Without it, a release says it deployed nothing.

Domain

The bare domain local.link points at the relay droplet, which owns /_link/* (agent connections, login, TLS checks) and /install.sh, and tunnels live on *.local.link. So the site can't simply take over local.link DNS. It lives at www.local.link instead. A CNAME for www points at Vercel, and the relay redirects browsers on the bare domain to it, through LINK_WEBSITE, which bootstrap's --website sets. The relay's own paths stay on the relay. The website has no settings. Its waitlist button links to a Tally form, which keeps what people enter, and the site stores nothing.

Layout

The words and the code are kept apart: every word is in a folder's docs/, and the code in src/ only lays them out. src/docs.ts reads every folder's docs/<language>/, and docs/en/website.md holds the site's name, tabs and words.

link-web holds no Rust, and link-core no TypeScript.

Decisions says why it works this way.