From e4400b6c9a222edb512e2d295cb802a4d86a28ab Mon Sep 17 00:00:00 2001 From: JMCP Bot Date: Wed, 25 Mar 2026 13:27:10 +0100 Subject: [PATCH] jmcp: Smoke-test /papers basePath with Telegram deep links --- .env.example | 2 + packages/config/src/index.ts | 2 + packages/contracts/src/index.ts | 32 +++++++++++++ packages/contracts/test/index.test.ts | 67 ++++++++++++++++++++++++++- 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 44c4667..ab199bd 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,8 @@ PAPERS_R2_ACCESS_KEY_ID= PAPERS_R2_SECRET_ACCESS_KEY= PAPERS_R2_BUCKET= PAPERS_R2_PUBLIC_BASE_URL= +PAPERS_PUBLIC_URL=http://localhost:3000 +PAPERS_BASE_PATH=/papers PAPERS_XAI_API_KEY= PAPERS_XAI_MODEL=grok-4-1-fast PAPERS_XAI_BASE_URL=https://api.x.ai/v1 diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index ec63861..d3e385b 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -34,6 +34,8 @@ const configSchema = z.object({ PAPERS_XAI_API_KEY: z.string().optional(), PAPERS_XAI_MODEL: z.string().default("grok-4-1-fast"), PAPERS_XAI_BASE_URL: z.string().default("https://api.x.ai/v1"), + PAPERS_PUBLIC_URL: z.string().default("http://localhost:3000"), + PAPERS_BASE_PATH: z.string().default("/papers"), TRIGGER_SECRET_KEY: z.string().optional(), TRIGGER_PROJECT_REF: z.string().optional(), }) diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 1da7fe7..96c91c7 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -402,6 +402,38 @@ export function slugify(value: string): string { .replace(/^-+|-+$/g, "") } +/** + * Build an absolute URL suitable for sharing externally (e.g. Telegram deep links). + * Next.js Link handles basePath for in-app navigation, but external URLs must + * include it manually. + */ +export function buildDeepLink(publicUrl: string, basePath: string, route: string): string { + const base = publicUrl.replace(/\/+$/, "") + const prefix = basePath.replace(/\/+$/, "") + const path = route.startsWith("/") ? route : `/${route}` + return `${base}${prefix}${path}` +} + +export function paperDeepLink(publicUrl: string, basePath: string, slug: string): string { + return buildDeepLink(publicUrl, basePath, `/papers/${slug}`) +} + +export function profileDeepLink(publicUrl: string, basePath: string, handle: string): string { + return buildDeepLink(publicUrl, basePath, `/u/${handle}`) +} + +export function feedDeepLink(publicUrl: string, basePath: string): string { + return buildDeepLink(publicUrl, basePath, "/feed") +} + +export function groupDeepLink(publicUrl: string, basePath: string, slug: string): string { + return buildDeepLink(publicUrl, basePath, `/groups/${slug}`) +} + +export function conferenceDeepLink(publicUrl: string, basePath: string, slug: string): string { + return buildDeepLink(publicUrl, basePath, `/conferences/${slug}`) +} + export const updateViewerProfileInputSchema = z.object({ headline: z.string().max(160), bio: z.string().max(2000), diff --git a/packages/contracts/test/index.test.ts b/packages/contracts/test/index.test.ts index 03fe40b..e8661ba 100644 --- a/packages/contracts/test/index.test.ts +++ b/packages/contracts/test/index.test.ts @@ -1,5 +1,13 @@ import { describe, expect, it } from "vitest" -import { serializePublicPaper } from "../src/index" +import { + buildDeepLink, + conferenceDeepLink, + feedDeepLink, + groupDeepLink, + paperDeepLink, + profileDeepLink, + serializePublicPaper, +} from "../src/index" describe("contracts", () => { it("removes the public author from blind papers", () => { @@ -45,3 +53,60 @@ describe("contracts", () => { expect(serializePublicPaper(paper).publicAuthorProfile).toBeNull() }) }) + +describe("deep links with /papers basePath", () => { + const url = "https://papers.example.com" + const basePath = "/papers" + + it("prefixes arbitrary routes with basePath", () => { + expect(buildDeepLink(url, basePath, "/feed")).toBe("https://papers.example.com/papers/feed") + }) + + it("handles routes without leading slash", () => { + expect(buildDeepLink(url, basePath, "feed")).toBe("https://papers.example.com/papers/feed") + }) + + it("strips trailing slashes from publicUrl", () => { + expect(buildDeepLink("https://papers.example.com/", basePath, "/feed")).toBe( + "https://papers.example.com/papers/feed", + ) + }) + + it("builds correct paper deep link", () => { + expect(paperDeepLink(url, basePath, "my-paper-slug")).toBe( + "https://papers.example.com/papers/papers/my-paper-slug", + ) + }) + + it("builds correct profile deep link", () => { + expect(profileDeepLink(url, basePath, "alice")).toBe( + "https://papers.example.com/papers/u/alice", + ) + }) + + it("builds correct feed deep link", () => { + expect(feedDeepLink(url, basePath)).toBe("https://papers.example.com/papers/feed") + }) + + it("builds correct group deep link", () => { + expect(groupDeepLink(url, basePath, "ml-reading-club")).toBe( + "https://papers.example.com/papers/groups/ml-reading-club", + ) + }) + + it("builds correct conference deep link", () => { + expect(conferenceDeepLink(url, basePath, "neurips-2026")).toBe( + "https://papers.example.com/papers/conferences/neurips-2026", + ) + }) + + it("works with empty basePath (no prefix deployment)", () => { + expect(paperDeepLink(url, "", "my-paper")).toBe("https://papers.example.com/papers/my-paper") + }) + + it("works with localhost for development", () => { + expect(feedDeepLink("http://localhost:3000", "/papers")).toBe( + "http://localhost:3000/papers/feed", + ) + }) +})