Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
07b02af
Use one spelling of the name and one description everywhere
ManulParihar Sep 7, 2026
42e16c2
Describe the company and site as structured data
ManulParihar Sep 7, 2026
b40ae4b
Move the FAQ into content so it has one source
ManulParihar Sep 7, 2026
c10d2c1
Describe the app and the FAQ as structured data on the home page
ManulParihar Sep 7, 2026
249b56c
Reference the shared publisher from blog post schema
ManulParihar Sep 7, 2026
fb28cd5
Record when content was last edited
ManulParihar Sep 7, 2026
a24ca8c
Give the sitemap real content dates
ManulParihar Sep 7, 2026
5e4ad9c
Report when a post was actually revised
ManulParihar Sep 7, 2026
c555de9
Record the intended blog cadence in the sitemap
ManulParihar Sep 7, 2026
8d6b1b7
Separate name spellings from other names for the product
ManulParihar Sep 7, 2026
001a3f3
Add /llms.txt
ManulParihar Sep 7, 2026
42db6d6
Name the AI crawlers in robots.txt
ManulParihar Sep 7, 2026
4178e94
Keep the payment callback out of search results
ManulParihar Sep 7, 2026
564f911
Cover xAI, Ai2 and the open-model training path
ManulParihar Sep 7, 2026
a1db786
Drop the guessed crawler names
ManulParihar Sep 7, 2026
5963a2f
Give the landing sections their own anchors
ManulParihar Sep 7, 2026
3db158a
Make every FAQ answer linkable
ManulParihar Sep 7, 2026
f9c50e0
Say what Kokio does in the hero
ManulParihar Sep 7, 2026
1401e9b
Render legal documents as plain text
ManulParihar Sep 7, 2026
f535428
Serve the whole site as one text file
ManulParihar Sep 7, 2026
c96fedc
Move the page text out of the full-text route
ManulParihar Sep 7, 2026
a8c08b8
Serve any page as markdown by adding .md to its URL
ManulParihar Sep 7, 2026
255b11c
Point readers at the markdown versions
ManulParihar Sep 7, 2026
002b389
Link the markdown version from the last three pages
ManulParihar Sep 7, 2026
9bff383
Check the built site before it ships
ManulParihar Sep 7, 2026
babdfaa
Remove the build dump and the template SVGs
ManulParihar Sep 7, 2026
0d09e70
Let eslint know the build scripts run in node
ManulParihar Sep 7, 2026
5eb56f5
Check definition anchors the same way as answer anchors
ManulParihar Sep 7, 2026
325114f
Add a glossary
ManulParihar Sep 7, 2026
cf959ff
Update the milestones
ManulParihar Sep 7, 2026
384e77c
Answer the roadmap and early access questions accurately
ManulParihar Sep 7, 2026
4b8a5fd
Put the milestones in the text versions of the site
ManulParihar Sep 7, 2026
5976674
Run lint in CI and give the site checks the history they need
ManulParihar Sep 7, 2026
387b656
Say the same thing about the launch in both places
ManulParihar Sep 7, 2026
6d19e16
Stop showing X twice in the banner
ManulParihar Sep 7, 2026
0a491c0
Fill in the liability cap and the dispute mechanism
ManulParihar Sep 7, 2026
57ffdbb
Use Kokio as the defined name in the legal documents
ManulParihar Sep 7, 2026
5ba1442
Use one spelling of the name in the blog posts
ManulParihar Sep 7, 2026
cd1e224
Warn when legal text runs ahead of its last review
ManulParihar Sep 7, 2026
376d460
Rename /blog to /blogs
ManulParihar Sep 8, 2026
f94e8fd
Fix blog images broken by the /blog redirect
ManulParihar Sep 8, 2026
1f93f32
Update blog index eyebrow to "Kokio Blogs"
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
8 changes: 8 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# The site checks that run after the build compare content dates
# against the commits that last touched that content, and a shallow
# clone has no commits to compare with.
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: npm
- run: npm ci
- run: npx tsc --noEmit
- run: npm run lint
# postbuild runs scripts/verify-site.mjs, so this step also fails on a
# page missing from the sitemap or a link that points at nothing.
- run: npm run build

audit:
Expand Down
7 changes: 5 additions & 2 deletions app/(legal)/privacy-policy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import { LegalDocumentView } from "@/components/legal/legal-document";
export const metadata: Metadata = {
title: "Privacy Policy",
description:
"How Koki'o handles information — a privacy-first, data-minimising eSIM app that collects as little about you as possible.",
alternates: { canonical: "/privacy-policy" },
"How Kokio handles information. A privacy-first, data-minimising eSIM app that collects as little about you as possible.",
alternates: {
canonical: "/privacy-policy",
types: { "text/markdown": "/privacy-policy.md" },
},
};

export default function PrivacyPolicyPage() {
Expand Down
7 changes: 5 additions & 2 deletions app/(legal)/terms-of-service/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { LegalDocumentView } from "@/components/legal/legal-document";

export const metadata: Metadata = {
title: "Terms of Service",
description: "The terms governing your use of the Koki'o app and services.",
alternates: { canonical: "/terms-of-service" },
description: "The terms governing your use of the Kokio app and services.",
alternates: {
canonical: "/terms-of-service",
types: { "text/markdown": "/terms-of-service.md" },
},
};

export default function TermsOfServicePage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
renderOgImage,
} from "@/lib/og-image";

export const alt = "Koki'o blog post";
export const alt = "Kokio blog post";
export const size = OG_IMAGE_SIZE;
export const contentType = OG_IMAGE_CONTENT_TYPE;

Expand All @@ -18,7 +18,7 @@ export default async function Image({
const post = getPost(slug);

return renderOgImage({
eyebrow: post?.tag ?? "Koki'o Blog",
title: post?.title ?? "Koki'o Blog",
eyebrow: post?.tag ?? "Kokio Blog",
title: post?.title ?? "Kokio Blog",
});
}
31 changes: 16 additions & 15 deletions app/blog/[slug]/page.tsx → app/blogs/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { WaveDivider } from "@/components/blog/wave-divider";
import { ReadingProgressBar } from "@/components/blog/reading-progress-bar";
import { BackToTopButton } from "@/components/blog/back-to-top-button";
import { getMdxComponents } from "@/components/blog/mdx-components";
import Logo from "@/assets/logo.svg";
import { ORGANIZATION_ID, WEBSITE_ID } from "@/lib/schema";

type Params = Promise<{ slug: string }>;

Expand All @@ -34,19 +34,23 @@ export async function generateMetadata({
const post = getPost(slug);
if (!post) return {};

const url = `${siteConfig.url}/blog/${post.slug}`;
const url = `${siteConfig.url}/blogs/${post.slug}`;

return {
title: `${post.title} | Kokio`,
title: post.title,
description: post.description,
alternates: { canonical: `/blog/${post.slug}` },
alternates: {
canonical: `/blogs/${post.slug}`,
types: { "text/markdown": `/blogs/${post.slug}.md` },
},
openGraph: {
type: "article",
url,
siteName: siteConfig.name,
title: post.title,
description: post.description,
publishedTime: post.date.toISOString(),
modifiedTime: post.updated.toISOString(),
authors: [post.author.name],
},
twitter: {
Expand Down Expand Up @@ -80,31 +84,28 @@ export default async function BlogPostPage({ params }: { params: Params }) {
const headings = frontmatter.headings ?? [];
const showToc = headings.length >= 5;

const url = `${siteConfig.url}/blog/${post.slug}`;
const url = `${siteConfig.url}/blogs/${post.slug}`;
const heroUrl = `${siteConfig.url}${post.ogImage ?? post.hero}`;
const logoUrl = `${siteConfig.url}${Logo.src}`;

const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
"@id": `${url}#article`,
headline: post.title,
description: post.description,
image: [heroUrl],
datePublished: post.date.toISOString(),
dateModified: post.date.toISOString(),
dateModified: post.updated.toISOString(),
inLanguage: "en",
author: {
"@type": "Person",
name: post.author.name,
...(post.author.twitter ? { sameAs: [post.author.twitter] } : {}),
},
publisher: {
"@type": "Organization",
name: siteConfig.name,
logo: {
"@type": "ImageObject",
url: logoUrl,
},
},
// Point at the Organization the layout already emits instead of describing
// it again here, so a post and the site cannot disagree about the publisher.
publisher: { "@id": ORGANIZATION_ID },
isPartOf: { "@id": WEBSITE_ID },
mainEntityOfPage: {
"@type": "WebPage",
"@id": url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import {
renderOgImage,
} from "@/lib/og-image";

export const alt = "Koki'o Blog";
export const alt = "Kokio Blog";
export const size = OG_IMAGE_SIZE;
export const contentType = OG_IMAGE_CONTENT_TYPE;

export default function Image() {
return renderOgImage({
eyebrow: "Koki'o Blog",
title: "Guides, updates, and stories from Koki'o",
eyebrow: "Kokio Blog",
title: "Guides, updates, and stories from Kokio",
});
}
9 changes: 5 additions & 4 deletions app/blog/page.tsx → app/blogs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ export const metadata: Metadata = {
description:
"Everything about eSIMs, travel connectivity, and what we're building behind the scenes.",
alternates: {
canonical: "/blog",
canonical: "/blogs",
types: {
"application/rss+xml": [{ url: "/blog/rss.xml", title: "Kokio Blog" }],
"application/rss+xml": [{ url: "/blogs/rss.xml", title: "Kokio Blog" }],
"text/markdown": "/blogs.md",
},
},
};
Expand All @@ -34,10 +35,10 @@ export default function BlogIndexPage() {
className="h-20 w-auto md:h-24"
/>
<p className="font-heading text-sm font-semibold uppercase tracking-wide text-cashmere-700">
KOKI&apos;O Blog
Kokio Blogs
</p>
<h1 className="font-heading text-4xl font-bold text-outer-space-950 md:text-5xl">
Guides, updates, and stories from Koki&apos;o
Guides, updates, and stories from Kokio
</h1>
<p className="max-w-xl text-base font-light text-esim-black-700 md:text-lg">
Everything about eSIMs, travel connectivity, and what we&apos;re
Expand Down
6 changes: 3 additions & 3 deletions app/blog/rss.xml/route.ts → app/blogs/rss.xml/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function GET() {

const items = posts
.map((post) => {
const url = `${siteConfig.url}/blog/${post.slug}`;
const url = `${siteConfig.url}/blogs/${post.slug}`;
return `
<item>
<title>${escapeXml(post.title)}</title>
Expand All @@ -32,8 +32,8 @@ export function GET() {
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Kokio Blog</title>
<link>${siteConfig.url}/blog</link>
<atom:link href="${siteConfig.url}/blog/rss.xml" rel="self" type="application/rss+xml" />
<link>${siteConfig.url}/blogs</link>
<atom:link href="${siteConfig.url}/blogs/rss.xml" rel="self" type="application/rss+xml" />
<description>${escapeXml(siteConfig.description)}</description>
<language>en-us</language>
<lastBuildDate>${lastBuildDate}</lastBuildDate>${items}
Expand Down
66 changes: 66 additions & 0 deletions app/glossary/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Metadata } from "next";

import { glossaryData } from "@/content/glossary";
import { siteConfig } from "@/config/site";
import { glossaryGraph } from "@/lib/schema";
import { PRODUCT_NAME } from "@/lib/site-copy";
import { JsonLd } from "@/components/json-ld";

const DESCRIPTION = `Plain definitions of the terms ${PRODUCT_NAME} uses: eSIM, eUICC, SM-DP+, LPA, passkey, smart wallet, no-KYC and more.`;

export const metadata: Metadata = {
title: "Glossary",
description: DESCRIPTION,
alternates: {
canonical: "/glossary",
types: { "text/markdown": "/glossary.md" },
},
openGraph: {
type: "article",
url: `${siteConfig.url}/glossary`,
siteName: PRODUCT_NAME,
title: `${PRODUCT_NAME} glossary`,
description: DESCRIPTION,
},
};

export default function GlossaryPage() {
return (
<main className="px-4 py-12 md:px-8 md:py-16">
<JsonLd data={glossaryGraph} />

<article className="mx-auto flex max-w-5xl flex-col rounded-4xl border border-esim-black-100 bg-background p-6 shadow-sm md:p-12 lg:p-16">
<header className="flex flex-col gap-3 border-b border-esim-black-100 pb-8">
<h1 className="font-heading text-4xl font-bold text-foreground md:text-5xl">
Glossary
</h1>
<p className="text-lg text-esim-black-700">{DESCRIPTION}</p>
</header>

{/* Each id is the target of the matching DefinedTerm @id, so a citation
can link to the one definition it used. */}
<dl className="mt-10 flex flex-col gap-10">
{glossaryData.map((entry) => (
<div
key={entry.id}
id={entry.id}
className="flex scroll-mt-24 flex-col gap-2"
>
<dt className="font-heading text-2xl font-bold text-foreground">
{entry.term}
{entry.aliases ? (
<span className="ml-3 font-sans text-base font-normal text-esim-black-700">
also {entry.aliases.join(", ")}
</span>
) : null}
</dt>
<dd className="text-lg font-light leading-8 text-esim-black-900">
{entry.definition}
</dd>
</div>
))}
</dl>
</article>
</main>
);
}
31 changes: 28 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import type { Metadata } from "next";
import { Anybody, Lexend } from "next/font/google";

import { cn } from "@/lib/utils";
import { siteConfig } from "@/config/site";
import { siteConfig, TWITTER_HANDLE } from "@/config/site";
import { META_DESCRIPTION, PRODUCT_NAME } from "@/lib/site-copy";
import { siteGraph } from "@/lib/schema";
import { JsonLd } from "@/components/json-ld";
import { NavBar } from "@/components/global/nav-bar";
import { Footer } from "@/components/global/footer";
import { Toaster } from "@/components/ui/toaster";
Expand All @@ -24,8 +27,29 @@ const lexend = Lexend({

export const metadata: Metadata = {
metadataBase: new URL(siteConfig.url),
title: "Kokio",
description: "Blockchain powered eSIM",
title: {
default: PRODUCT_NAME,
template: `%s | ${PRODUCT_NAME}`,
},
description: META_DESCRIPTION,
applicationName: PRODUCT_NAME,
alternates: {
canonical: "/",
types: { "text/markdown": "/index.md" },
},
openGraph: {
type: "website",
url: siteConfig.url,
siteName: PRODUCT_NAME,
title: PRODUCT_NAME,
description: META_DESCRIPTION,
},
twitter: {
card: "summary_large_image",
site: TWITTER_HANDLE,
title: PRODUCT_NAME,
description: META_DESCRIPTION,
},
};

export default function RootLayout({
Expand All @@ -44,6 +68,7 @@ export default function RootLayout({
"flex min-h-screen flex-col bg-beach-sky font-sans antialiased"
)}
>
<JsonLd data={siteGraph} />
<AnnouncementBanner />
<NavBar />
<div className="m-0 flex-1 p-0">{children}</div>
Expand Down
40 changes: 40 additions & 0 deletions app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import sitemap from "@/app/sitemap";
import { siteConfig } from "@/config/site";
import { PRODUCT_NAME } from "@/lib/site-copy";
import { pageSections } from "@/lib/site-text";

export const dynamic = "force-static";

/**
* Every page of the site as one file, so an agent gets a complete picture from
* a single request instead of giving up part way through crawling eight routes.
*
* Sections are driven by sitemap.xml rather than listed by hand: a page that
* reaches the sitemap without text here fails the build instead of quietly
* going missing.
*/
export function GET() {
const sections = pageSections();

const body = sitemap().map(({ url }) => {
const { pathname } = new URL(url);
const render = sections[pathname];
if (!render) {
throw new Error(
`llms-full.txt has no section for ${pathname}. Add one in lib/site-text.ts or drop the page from the sitemap.`
);
}
return `# ${pathname}\n\n${render()}`;
});

const header = [
`# ${PRODUCT_NAME}: full site text`,
`> Every page of ${siteConfig.url} as one markdown file. Each section below is headed by the path it was rendered from. Any single page is also available on its own by adding .md to its URL.`,
].join("\n\n");

return new Response(`${header}\n\n---\n\n${body.join("\n\n---\n\n")}\n`, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
},
});
}
Loading
Loading