The landing page and OAuth callback relay for Orange Cloud, deployed at o-c.do on Cloudflare Workers.
- Landing page in five locales (en / zh-Hans / zh-Hant / zh-HK / ja) via
next-intl, withlocalePrefix: "as-needed"— English lives at/, other locales at/zh-Hansetc. Also serves/privacy,/terms, and/contact, which the iOS app links to. - OAuth callback relay at
/oauth/callback(route.ts): Cloudflare's OAuth only acceptshttpsredirect URIs, so this route 302-redirects the authorizationcodeandstatestraight to the iOS app's custom scheme (orangecloud://oauth/callback). It stores nothing and never exchanges the code — the token exchange andstatevalidation happen on-device, secured by PKCE.
- Next.js 16 (App Router) + React 19
next-intlfor routing/messages — translations live insrc/messages/- Tailwind CSS 4
- Deployed to Cloudflare Workers with
@opennextjs/cloudflare; config inwrangler.jsonc
/oauth/callbacksits outside the[locale]segment, and the middleware matcher insrc/middleware.tsexplicitly excludesoauth— the callback must reach the route handler untouched. Don't let locale routing swallow it.- On Cloudflare Workers, next-intl's middleware must be an edge
middleware.ts— the Next 16proxy.tsconvention runs on the Node runtime, which Workers doesn't support. - The screenshot gallery images live in
public/shots/<locale>/(01_dashboard.jpg…); zh-HK reuses the zh-Hant set.
What's wired up:
-
robots.tstiers AI crawlers: search/retrieval bots (OAI-SearchBot, Claude-SearchBot, PerplexityBot) and user-triggered bots (ChatGPT-User, Claude-User, Perplexity-User) are allowed; training crawlers (GPTBot, ClaudeBot, Meta-ExternalAgent, CCBot), opt-out tokens (Google-Extended, Applebot-Extended), and undeclared crawlers (Bytespider) are blocked./oauth/is excluded everywhere. -
public/llms.txt— Markdown overview of the product, key pages, pricing, and the GitHub repo for AI retrieval (spec). -
sitemap.tsemits all pages with full hreflang alternates;generateMetadatacovers canonical, hreflang, Open Graph, Twitter card, anditunesapp association. -
JSON-LD (
SoftwareApplication) in the locale layout — only the Bing/Copilot index-enrichment path is known to consume it, so it stays minimal. -
IndexNow — changed pages are pushed to Bing (and Yandex / Seznam / Naver / Yep, which share the protocol) automatically:
- the URL list is generated from
src/lib/site/urls.ts, the same sourcesitemap.tsreads. Each entry carries anupdateddate — bump it when you change a page, that date is both the sitemaplastmodand the "has this changed?" test; - the daily cron in
custom-worker.ts(UTC 08:00) diffs that list against theindexnow_urlsledger in D1 and submits only what changed, then records it. Nothing changed → no outbound request; - submissions go to
bing.com/indexnowfirst, falling back toapi.indexnow.orgthenyandex.com/indexnow— the protocol shares a submission with every participating engine, so the first 2xx is enough. The neutralapi.indexnow.orgendpoint 429s from Cloudflare Workers egress IPs (measured 2026-08-21: 429 for a single URL with noRetry-After, while the identical body from a residential IP got 202) — that's why it isn't first; - the key file
public/ae4368227a78d73327c42c34949e9075.txtis how search engines verify the domain. Don't delete or rename it.
To push immediately after publishing (instead of waiting for the cron), with the admin password:
curl -X POST "https://o-c.do/api/indexnow" -H "Authorization: Bearer $ADMIN_PASSWORD"
GET /api/indexnowpreviews what would be submitted without sending anything; add?force=1to either to ignore the ledger and resubmit everything. - the URL list is generated from
One-time manual steps (dashboard accounts required): verify the domain in Google Search Console and Bing Webmaster Tools, submit https://o-c.do/sitemap.xml in both, and optionally list llms.txt at directory.llmstxt.cloud / llmstxt.site.
From the repo root (pnpm workspace):
pnpm install
pnpm dev # turbo dev --filter=web → next dev on http://localhost:3000Or inside apps/web/:
pnpm dev # Next.js dev server
pnpm preview # build with OpenNext and preview on the Workers runtime
pnpm cf-typegen # regenerate cloudflare-env.d.ts after changing wrangler.jsoncpnpm deploy # opennextjs-cloudflare build && deployThe official deployment uses the custom domain o-c.do (configured in wrangler.jsonc routes). For your own fork: change the name and routes in wrangler.jsonc, deploy under your own Cloudflare account, then register https://<your-domain>/oauth/callback as the redirect URI of your own Cloudflare OAuth client (see CONTRIBUTING.md).