Technical Writer
Documentation for Drash lives in docs/, a Nextra 4 site built with Next.js and
exported statically. Follow this when writing or editing any page under
docs/content/.
Voice
- Concrete over persuasive. Name the mechanism, not the feeling. "A strict resource interface forces separation of concerns" beats "clean, maintainable architecture."
- No filler. Cut "powerful", "seamless", "simply", "just", "easily". If a sentence survives deleting the adjective, delete it.
- Concede real limits. A stated trade-off is more convincing than an unqualified claim. Saying the only lock-in is your runtime's is stronger than claiming there is none.
- Second person for instructions, present tense for behaviour. "You hand the application a request." "The application rejects on error."
Assume the reader knows nothing
Write for someone meeting the subject for the first time. They have not read the other pages, they do not know Drash's vocabulary, and they will not infer what you left out — they will leave.
- Define a term the first time a page uses it, or link to where it is defined. "Resource", "handler", "chain", "middleware", "entry point", and "resource group" are Drash words, not English ones. A page may use them freely after it has said what they mean.
- Say what a thing does before naming it.
ResourcesIndexandAbstractChainBuilderare labels for machinery the reader has never seen. "The handler that matches a request URL to a resource (ResourcesIndex)" teaches; "ResourcesIndexcaches by fully-qualified URL" does not. - Never write "as described above" across pages. Each page is someone's first. Link instead — the reader chooses whether to follow it.
- Show the whole thing, then take it apart. A complete, runnable sample first; the explanation of each piece after. A reader who cannot see the shape of the finished thing cannot place the parts.
- Spell out the commands. Do not assume the reader knows the package
manager, the runtime's flags, or that
--allow-netis why the server can listen. Show the command that works and say what it needs. - State the prerequisite instead of implying it. If a step only works with a runtime installed or a file already created, say so at that step.
- "Simply", "just", "obviously", "of course" are banned — the No filler rule covers them, but the reason here is different. To a reader who is stuck, those words say the difficulty is theirs.
The test: could someone who has never used Drash read this page start to finish and end up with something that runs, without needing a tab you did not link?
Verify before asserting
Docs that describe code must be checked against the code.
- API surface: read
src/— do not infer from another doc page. Entry-point exports live insrc/modules/http.native.tsandhttp.polyfill.ts; the chain builder insrc/modules/builders/RequestChainBuilder.ts. - Versions: take them from CI (
.github/workflows/*.yml) orpackage.json. If a number cannot be sourced, say what is tested rather than inventing a minimum. - The published package lags the source. Docs show the source API:
@drashland/drash/modules/http.{native,polyfill}.js, exportingApplication. The last published build still uses the oldermodules/chains/RequestChain/mod.*path and exportsChain, so anything resolving against npm or esm.sh — the apps underexamples/— will not match the docs until the next release. Write the source API and keep a page internally consistent. - Error messages: quote them from source, not memory.
One fact, one page
The site splits by intent. Putting the same explanation in two places means both rot.
| Section | Holds |
|---|---|
content/docs/concepts/ |
Why it works that way |
content/docs/quickstart/ |
A complete app per runtime |
content/docs/*.mdx |
A single top-level subject (Error Handling) |
content/reference/ |
API surface, by Core / Standard / Modules |
content/examples/ |
Finished apps you can run |
Cross-link instead of repeating. When merging pages, delete the duplicate rather than keeping both phrasings.
Nextra mechanics
Components are global — no imports needed. mdx-components.js injects
Callout, Cards, Compare, CompareItem, FileTree, SeeAlso, Steps,
Tabs, and the table elements.
<Callout type="info" | "warning" | "gray">—grayis a local component.<Cards>— title-only on this site. Passing children switches the card to a filled style and breaks visual consistency with every other card.<Tabs items={[…]}>— put shared headings outside the group. A###inside four tabs produces four TOC entries (#steps,#steps-1, …). Two groups can be synced with a matchingstorageKey, but one group is better when the reader should choose once.
_meta.js controls order and labels. Keys are file basenames; order in the
file is order in the sidebar.
- A folder that appears first in its parent
_meta.jsmust contain anindex.mdx. The breadcrumb resolves to the folder's own route, and a folder with no index 404s. display: "hidden"keeps a page out of the sidebar; omittypeto keep it out of the top bar too.- Use
"quoted-keys"for hyphenated basenames, matching the other_meta.jsfiles.
Code fences support filename=, showLineNumbers, and highlight ranges:
```ts filename="app.ts" showLineNumbers {2,13-17}
Inside a list item, indent the fence 4 spaces so it stays in the item. When a step adds to the previous one, highlight only the added lines. Never delete explanatory comments from a sample to make it shorter, and keep trailing comments aligned to one column per block.
Links
- Site-relative, no origin:
/docs/concepts/chains. - Verify the route exists. Folder routes without an
index.mdxare not pages —/reference/core404s while/reference/core/http-errorworks. - Anchors are slugified with punctuation stripped:
.urlPatternClass(x)becomes#urlpatternclassurlpatternclass. Confirm the id in the built HTML. - There are no redirects. The site is
output: "export", so renaming or moving a page breaks every old URL. When moving one, rewrite every inbound link in the same change.
Before calling it done
cd docs && pnpm build # MDX and JSX errors surface here
deno fmt --check && deno lint # from the repo root
deno fmt does not cover .mdx, so table alignment there is cosmetic only.
Then run the link check over the built output — it catches the moved-page and missing-index cases that a grep will not:
cd docs && python3 - <<'PY'
import re, os, glob
def exists(p):
p = p.split('#')[0].split('?')[0]; r = p.strip('/')
if r == '': return os.path.isfile('out/index.html')
return any(os.path.isfile(c) for c in
(f'out/{r}/index.html', f'out/{r}', f'out/{r}.html'))
broken, total = {}, 0
for f in glob.glob('out/**/*.html', recursive=True):
h = open(f, encoding='utf-8', errors='ignore').read()
# `(?<!data-)` skips Nextra's data-href, which is metadata, not a link
for href in set(re.findall(r'(?<!data-)href="(/[^"]*)"', h)):
if href.startswith('//') or href.startswith('/_next'): continue
total += 1
if not exists(href): broken.setdefault(href, set()).add(f)
print(f"links={total}", "-> 0 broken" if not broken
else "-> BROKEN: " + str({k: len(v) for k, v in broken.items()}))
PY
Report the counts. A page that builds is not a page that works.