Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
9c596cb
Remove the Docusaurus template pages and assets
ManulParihar Sep 7, 2026
aca2cec
Remove the duplicated smart contract pages
ManulParihar Sep 7, 2026
c7ca7cd
Link the two pages that had no sidebar entry
ManulParihar Sep 7, 2026
3ad3a6c
Point the wiki links at pages people can read
ManulParihar Sep 7, 2026
a62b0a9
Fix the head tags on every page
ManulParihar Sep 7, 2026
0971e3a
Fix typos and broken frontmatter in the docs
ManulParihar Sep 7, 2026
dabb498
Give the sitemap real dates
ManulParihar Sep 7, 2026
141584c
Add robots.txt and say yes to AI crawlers
ManulParihar Sep 7, 2026
3d1e058
Serve every page as plain markdown, and all of them as one file
ManulParihar Sep 7, 2026
b218f08
Add llms.txt
ManulParihar Sep 7, 2026
afce9c9
Commit explicit heading ids
ManulParihar Sep 7, 2026
27415f3
Use one spelling of the name, and drop the filler
ManulParihar Sep 7, 2026
903f766
Describe each page in JSON-LD
ManulParihar Sep 7, 2026
cde1c7b
Install and build with npm
ManulParihar Sep 7, 2026
3094a41
Define the site node once, on every page
ManulParihar Sep 7, 2026
9f4f40d
Say what the product actually does on the home page
ManulParihar Sep 7, 2026
381740f
Pin every dependency to an exact version
ManulParihar Sep 7, 2026
4b7a8c3
Check the built site before it ships
ManulParihar Sep 7, 2026
00cb0bb
Give the contract pages their final paths
ManulParihar Sep 7, 2026
4aea11f
Publish the Base Sepolia addresses
ManulParihar Sep 7, 2026
0748e22
Do not fail the build on a page that has no commit yet
ManulParihar Sep 7, 2026
ba42f7c
Document the TypeScript SDK
ManulParihar Sep 7, 2026
e0bf4fa
Add the missing heading ids on the deployments page
ManulParihar Sep 7, 2026
6c07b85
Drop the calldata builder page
ManulParihar Sep 7, 2026
210514c
Document every contract in the suite
ManulParihar Sep 7, 2026
7c6e286
Rewrite the older pages so each one answers on its own
ManulParihar Sep 7, 2026
848ecce
Fail the build on a page too short to answer on its own
ManulParihar Sep 7, 2026
a148690
Check page naming and wording before the build runs
ManulParihar Sep 7, 2026
b3a29f7
Write out what the two diagrams show
ManulParihar Sep 7, 2026
49a5fb3
Say what the architecture diagram actually shows
ManulParihar Sep 7, 2026
ba3bd7d
Sketch x402 as future work
ManulParihar Sep 7, 2026
b468864
Override three vulnerable build-time dependencies
ManulParihar Sep 8, 2026
3c9d9e7
Drop the italics docgen wraps a dev note in
ManulParihar Sep 8, 2026
d774346
Say the x402 idea plainly
ManulParihar Sep 8, 2026
90ad62d
Repaint the docs in Kokio's colours
ManulParihar Sep 8, 2026
06e37df
Point the site chrome at Kokio's fonts, mark and links
ManulParihar Sep 8, 2026
df04f4e
Rebuild the home page on the blog's hero
ManulParihar Sep 8, 2026
4f65c04
Stop Home reading as active on every page
ManulParihar Sep 8, 2026
a460919
Add Manifesto to the footer More column
ManulParihar Sep 8, 2026
f8e8534
Add search to the docs header
ManulParihar Sep 8, 2026
67b668a
Answer docs questions from an endpoint
ManulParihar Sep 8, 2026
dc04cf6
Put the answer box in the header
ManulParihar Sep 8, 2026
4319dd4
Read the docs from disk instead of over the network
ManulParihar Sep 8, 2026
9364b8e
Add a way to ask a question without deploying
ManulParihar Sep 8, 2026
8f25c53
Fall back to the live site, not to the deployment itself
ManulParihar Sep 8, 2026
2bc5c7d
Retry a busy model before giving up
ManulParihar Sep 8, 2026
887402f
Stop answers being cut off mid-sentence
ManulParihar Sep 8, 2026
bb334b5
Make the model call survive its own failures
ManulParihar Sep 8, 2026
8dcedcb
Bound what the answer endpoint will do for a stranger
ManulParihar Sep 8, 2026
61ad1aa
Post every search question to Discord
ManulParihar Sep 8, 2026
2b86b7f
Say why nothing reached Discord
ManulParihar Sep 8, 2026
a17561d
Send the Discord post before responding
ManulParihar Sep 8, 2026
93603c8
Colour the Discord posts by outcome
ManulParihar Sep 8, 2026
d2bb18e
Try the second model when the daily quota runs out
ManulParihar Sep 8, 2026
b25235c
Removed obsolete diagram
ManulParihar Sep 8, 2026
c304d20
Merge pull request #47 from Blockchain-Powered-eSIM/upgrade/ai-seo
ManulParihar Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# The sitemap reads each page's last commit date from git. The default
# shallow clone has no history, so every lastmod would be missing.
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
- run: yarn install
- run: yarn build
- run: npm ci
- run: npm run build
# The build only warns when a contract page is behind its source repo. Here
# a person is already looking at a diff, so it fails instead.
- run: node scripts/sync-reference.mjs --strict
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
.docusaurus
.cache-loader

# Written by scripts/build-text.mjs on every build
/static/md
/static/llms-full.txt

# Misc
.DS_Store
.env.local
Expand All @@ -16,7 +20,3 @@
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

.yarn
38 changes: 21 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,45 @@
# Website
# Kokio documentation

This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
Source for [docs.kokio.app](https://docs.kokio.app), built with [Docusaurus](https://docusaurus.io/).

### Installation
npm only. There is one lockfile, `package-lock.json`, and Vercel picks the package manager from it.

### Install

```
$ yarn
npm ci
```

### Local Development
### Local development

```
$ yarn start
npm start
```

This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
Starts a dev server on port 3000 and reloads on save.

### Build

```
$ yarn build
npm run build
```

This command generates static content into the `build` directory and can be served using any static contents hosting service.

### Deployment
Writes the static site to `build/`. The prebuild step regenerates the plain-text surfaces that AI crawlers read: one `.md` file per page under `static/md/`, plus `static/llms-full.txt`. Both are gitignored, so build before serving locally or those routes 404.

Using SSH:
Serve the result with:

```
$ USE_SSH=true yarn deploy
npm run serve
```

Not using SSH:
### Deployment

Vercel builds `main` and deploys it. Nothing to run by hand. Open a PR, merge it, and the change is live.

### Other commands

```
$ GIT_USER=<Your GitHub username> yarn deploy
npm run typecheck # tsc, no emit
npm run write-heading-ids # regenerate explicit heading anchors after editing headings
npm run clear # drop the .docusaurus cache
```

If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
194 changes: 194 additions & 0 deletions api/ask.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* POST /api/ask, the endpoint behind the search box.
*
* Takes a question, returns an answer written from the docs plus the pages it
* came from. When the model cannot answer, the pages still come back, so the
* box degrades into plain search rather than into an error.
*
* The response never carries the reason. It can name the key or the model, so
* it goes to the logs instead.
*
* Every question is also posted to Discord when `DISCORD_WEBHOOK_URL` is set,
* which is how gaps in the docs get noticed.
*/

import { answerQuestion, MAX_QUESTION_LENGTH } from "../lib/docs-answer.mjs";
import { SITE_URL } from "../src/siteCopy.mjs";

const WINDOW_MS = 60 * 1000;

/** Long enough for Discord on a bad day, short enough to never matter. */
const NOTIFY_TIMEOUT_MS = 1500;

/** Discord's limits on the parts of an embed this uses. */
const EMBED_TITLE_MAX = 250;
const EMBED_DESCRIPTION_MAX = 4000;
const EMBED_FIELD_MAX = 1000;

/** Discord's own green and red, which stay legible in both themes. */
const COLOUR_ANSWERED = 0x57f287;
const COLOUR_UNANSWERED = 0xed4245;

function clip(text, max) {
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
}

/** Per reader, and for everyone this instance is serving. */
const MAX_PER_ADDRESS = 8;
const MAX_PER_INSTANCE = 60;

const recent = new Map();
let instanceHits = [];
let warnedNoWebhook = false;

/**
* Requests per address, kept in memory.
*
* Serverless spreads traffic across instances, so this slows one impatient
* reader rather than a determined attacker. The instance cap below is the
* cost stop, and the model's own daily quota is the one behind that.
*/
function overLimit(address, now) {
instanceHits = instanceHits.filter((at) => now - at < WINDOW_MS);
instanceHits.push(now);
if (instanceHits.length > MAX_PER_INSTANCE) return "instance";

const seen = (recent.get(address) ?? []).filter((at) => now - at < WINDOW_MS);
seen.push(now);
recent.set(address, seen);
// Addresses stop arriving but never leave, so drop the lot now and then.
if (recent.size > 5000) recent.clear();

return seen.length > MAX_PER_ADDRESS ? "address" : null;
}

/**
* Rejects a browser on another site posting here.
*
* A request with no Origin, such as curl or a test script, is allowed: the
* header only proves where a browser came from, and blocking its absence
* stops honest tools without stopping anyone else.
*/
function fromAnotherSite(request) {
const origin = request.headers.origin;
if (!origin) return false;
try {
return new URL(origin).host !== request.headers.host;
} catch {
return true;
}
}

/**
* Posts what was asked and what came back to a Discord channel.
*
* Sent before the response, not after. A serverless function can be frozen the
* moment it responds, which leaves the post half sent and the channel empty.
* The timeout below is what keeps that from costing the reader anything.
*
* Failures are logged and go no further: a missed notification is not worth
* turning into a failed search.
*/
async function notify(question, { answer, sources, degraded }) {
const url = process.env.DISCORD_WEBHOOK_URL;
if (!url) {
// Once per instance. Silence here is indistinguishable from a webhook that
// is set but refused, and both look the same from the channel.
if (!warnedNoWebhook) {
warnedNoWebhook = true;
console.error("discord notify skipped: DISCORD_WEBHOOK_URL is not set");
}
return;
}

const embed = {
// The stripe down the side is the whole point: a channel of these can be
// skimmed for the red ones without reading a word.
color: answer ? COLOUR_ANSWERED : COLOUR_UNANSWERED,
title: clip(question, EMBED_TITLE_MAX),
description: clip(answer ?? "No answer. Closest pages below.", EMBED_DESCRIPTION_MAX),
footer: { text: answer ? "answered" : (degraded ?? "no match") },
timestamp: new Date().toISOString(),
};

const links = sources
.map((source) => `[${source.title}](${SITE_URL}${source.url})`)
.join(" · ");
if (links) {
embed.fields = [{ name: "Pages", value: clip(links, EMBED_FIELD_MAX) }];
}

try {
const posted = await fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
embeds: [embed],
// The question is whatever a reader typed, so it must not be able to
// ping the server.
allowed_mentions: { parse: [] },
}),
signal: AbortSignal.timeout(NOTIFY_TIMEOUT_MS),
});
// A deleted or mistyped webhook answers 401 or 404 rather than throwing,
// so without this the channel just stays empty.
if (!posted.ok) {
console.error(`discord notify rejected: ${posted.status} ${await posted.text()}`);
}
} catch (error) {
console.error("discord notify failed:", error.message);
}
}

function send(response, status, body) {
response.status(status);
response.setHeader("content-type", "application/json; charset=utf-8");
response.setHeader("cache-control", "no-store");
response.end(JSON.stringify(body));
}

export default async function handler(request, response) {
if (request.method !== "POST") return send(response, 405, { error: "use POST" });
if (fromAnotherSite(request)) return send(response, 403, { error: "wrong origin" });

const type = request.headers["content-type"] ?? "";
if (!type.includes("application/json")) {
return send(response, 415, { error: "send JSON" });
}

const address =
request.headers["x-forwarded-for"]?.split(",")[0]?.trim() ?? "unknown";
const limit = overLimit(address, Date.now());
if (limit) {
if (limit === "instance") console.error("ask: instance rate limit hit");
return send(response, 429, { error: "rate_limited" });
}

let question = "";
try {
const body =
typeof request.body === "string" ? JSON.parse(request.body) : request.body;
question = String(body?.question ?? "").trim();
} catch {
return send(response, 400, { error: "body must be JSON" });
}

if (!question) return send(response, 400, { error: "question is required" });
if (question.length > MAX_QUESTION_LENGTH) {
return send(response, 400, { error: "question is too long" });
}

try {
const { answer, sources, degraded, detail, cached } = await answerQuestion(question, {
apiKey: process.env.GEMINI_API_KEY,
});
if (detail) console.error("ask:", degraded ?? "answered", detail);

// A repeat inside the cache window was posted the first time it was asked.
if (!cached) await notify(question, { answer, sources, degraded });
return send(response, 200, { answer, sources, degraded });
} catch (error) {
console.error("ask failed:", error);
return send(response, 500, { error: "could not answer" });
}
}
12 changes: 0 additions & 12 deletions blog/2019-05-28-first-blog-post.md

This file was deleted.

44 changes: 0 additions & 44 deletions blog/2019-05-29-long-blog-post.md

This file was deleted.

20 changes: 0 additions & 20 deletions blog/2021-08-01-mdx-blog-post.mdx

This file was deleted.

Binary file not shown.
25 changes: 0 additions & 25 deletions blog/2021-08-26-welcome/index.md

This file was deleted.

Loading
Loading