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'slink-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, aslink-harnessdoes indocs/en/harnesses/link/, and an old address of a page that moved redirects to it. - A
README.mdoutsidedocs/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/'sSECURITY.md,SUPPORT.mdandCODE_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 devTo 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 buildAdd 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.mdIts 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 Markdown | On the website |
|---|---|
The first # heading | The page's title, or its tab's name if it's a tab |
Each ## heading | A line under the page in the tree |
| The first paragraph | The page's description |
--- | A break between cards |
[Join Waitlist](https://... "button") alone in a paragraph | The big pill button |
A list of links alone between two --- | A row of pills |
A sh code block | Commands with a copy button; other blocks are code |
<!-- ... --> | Hidden in both |
A relative link to a .md file | A link to its page, anchor kept |
A link to a page on https://www.local.link | A link to that page, in the reader's language |
| A relative link outside the docs | The file on GitHub, marked as leaving the site |
| A link to another site | A 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 collectcollect 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:
- In Vercel: Add New → Project, import the repository.
- Set Root Directory to
link-web.vercel.jsonpins 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'sdocs/, outside the Root Directory, so keep "Include files outside the root directory in the Build Step" on, as it is by default. - In Settings → Git → Deploy Hooks, make a hook for
main, and save its URL as theVERCEL_DEPLOY_HOOKsecret of the repository'sreleaseenvironment. 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.
