Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Tests that need a Docker daemon skip themselves when the socket is missing, so a
| `apps/gateway/test/integration/` | A real HTTP server on a real socket, driven end to end |
| `apps/web/` | The admin UI, Vue 3 with shadcn-vue components |
| `apps/web/scripts/` | Maintenance for the source catalog, run from CI |
| `apps/site/` | The landing page and rendered documentation, Astro with Vue islands, deployed to Cloudflare Pages |
| `packages/schema/` | Zod schemas and DTO types shared by the gateway and the UI |
| `docs/` | Reference pages and deployment guides linked from the README |
| `.github/` | CI workflows, issue and pull request templates, and the community health files |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ apps/gateway/data/
junctio
.prototype/
.cache/
.astro/
12 changes: 12 additions & 0 deletions apps/site/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Junctio site

The landing page and the rendered documentation at junctio.pages.dev, built with Astro, Tailwind 4 and Vue islands.

The documentation pages are generated from `../../docs/*.md` at build time; nothing under `src/content` duplicates them. Use-case pages live in `src/content/use-cases` and are the only prose authored here.

```bash
bun run dev:site # astro dev server on 4321
bun run build:site # static output in apps/site/dist
```

Cloudflare Pages settings: root directory `apps/site`, build command `bun run build`, output directory `dist`. Set `SITE_URL` once the site has its own domain so canonical URLs and the sitemap follow it.
25 changes: 25 additions & 0 deletions apps/site/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineConfig } from "astro/config";
import { unified } from "@astrojs/markdown-remark";
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import vue from "@astrojs/vue";
import tailwindcss from "@tailwindcss/vite";
import { rehypeDocLinks } from "./src/lib/rehype-doc-links.ts";

export default defineConfig({
site: process.env.SITE_URL ?? "https://junctio.pages.dev",
trailingSlash: "always",
integrations: [vue(), mdx(), sitemap()],
markdown: {
processor: unified({ rehypePlugins: [rehypeDocLinks] }),
shikiConfig: {
themes: { light: "github-light", dark: "github-dark" }
}
},
vite: {
plugins: [tailwindcss()],
server: {
fs: { allow: ["../.."] }
}
}
});
31 changes: 31 additions & 0 deletions apps/site/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@junctio/site",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"typecheck": "astro check"
},
"dependencies": {
"@astrojs/markdown-remark": "^7.3.1",
"@astrojs/mdx": "^8.0.1",
"@astrojs/sitemap": "^3.7.4",
"@astrojs/vue": "^7.0.3",
"@lucide/vue": "^1.46.0",
"@tailwindcss/vite": "^4.3.3",
"astro": "^7.3.3",
"tailwindcss": "^4.3.3",
"unist-util-visit": "^5",
"vfile": "^6",
"vue": "^3.5.42"
},
"devDependencies": {
"@astrojs/check": "^0.9.10",
"@types/hast": "^3",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3"
}
}
1 change: 1 addition & 0 deletions apps/site/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/site/public/image.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/site/public/logo-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/site/public/logo-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions apps/site/src/components/CopyCommand.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { Check, Copy } from "@lucide/vue";
import { ref } from "vue";

const props = defineProps<{ command: string; label?: string }>();
const copied = ref(false);

async function copy() {
try {
await navigator.clipboard.writeText(props.command);
copied.value = true;
setTimeout(() => {
copied.value = false;
}, 1600);
} catch {}
}
</script>

<template>
<div class="flex items-center gap-2 overflow-hidden rounded-lg border bg-card pl-3.5 font-mono text-[13px]">
<span class="select-none text-muted-foreground">$</span>
<code class="min-w-0 flex-1 truncate py-2.5">{{ command }}</code>
<button
type="button"
class="flex h-full shrink-0 items-center gap-1.5 border-l px-3 py-2.5 font-sans text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
:aria-label="label ?? 'Copy command'"
@click="copy"
>
<Check v-if="copied" class="size-3.5" />
<Copy v-else class="size-3.5" />
{{ copied ? "Copied" : "Copy" }}
</button>
</div>
</template>
98 changes: 98 additions & 0 deletions apps/site/src/components/DocsLayout.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
import Base from "@/layouts/Base.astro";
import { DOCS_NAV } from "@/lib/site";
import { getCollection } from "astro:content";

interface Props {
title: string;
description: string;
current: string;
headings?: { depth: number; slug: string; text: string }[];
editUrl?: string;
}

const { title, description, current, headings = [], editUrl } = Astro.props;
const useCases = (await getCollection("useCases")).sort((a, b) => a.data.order - b.data.order);
const toc = headings.filter((heading) => heading.depth === 2);
---

<Base title={title} description={description} ogType="article">
<div class="mx-auto grid max-w-6xl gap-10 px-4 py-10 sm:px-6 lg:grid-cols-[220px_minmax(0,1fr)_180px]">
<aside class="lg:sticky lg:top-24 lg:self-start">
<nav class="space-y-6 text-sm">
{
DOCS_NAV.map((group) => (
<div>
<p class="mb-2 text-xs font-semibold tracking-wide text-muted-foreground uppercase">{group.label}</p>
<ul class="space-y-0.5">
{group.items.map((item) => (
<li>
<a
href={`/docs/${item.slug ? `${item.slug}/` : ""}`}
class:list={[
"block rounded-md px-2 py-1.5 transition-colors hover:bg-accent",
current === item.slug ? "bg-accent font-medium" : "text-muted-foreground"
]}
>
{item.title}
</a>
</li>
))}
</ul>
</div>
))
}
<div>
<p class="mb-2 text-xs font-semibold tracking-wide text-muted-foreground uppercase">Use cases</p>
<ul class="space-y-0.5">
{
useCases.map((entry) => (
<li>
<a
href={`/use-cases/${entry.id}/`}
class:list={[
"block rounded-md px-2 py-1.5 transition-colors hover:bg-accent",
current === `use-cases/${entry.id}` ? "bg-accent font-medium" : "text-muted-foreground"
]}
>
{entry.data.title}
</a>
</li>
))
}
</ul>
</div>
</nav>
</aside>
<article class="prose-docs min-w-0">
<slot />
{
editUrl && (
<p class="mt-12 border-t pt-4 text-sm text-muted-foreground">
<a href={editUrl} target="_blank" rel="noreferrer">
Edit this page on GitHub
</a>
</p>
)
}
</article>
<aside class="hidden lg:sticky lg:top-24 lg:block lg:self-start">
{
toc.length > 0 && (
<nav class="text-sm">
<p class="mb-2 text-xs font-semibold tracking-wide text-muted-foreground uppercase">On this page</p>
<ul class="space-y-1.5">
{toc.map((heading) => (
<li>
<a href={`#${heading.slug}`} class="text-muted-foreground transition-colors hover:text-foreground">
{heading.text}
</a>
</li>
))}
</ul>
</nav>
)
}
</aside>
</div>
</Base>
18 changes: 18 additions & 0 deletions apps/site/src/components/Footer.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
import { SITE } from "@/lib/site";
---

<footer class="border-t">
<div
class="mx-auto flex max-w-6xl flex-col gap-4 px-4 py-8 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between sm:px-6"
>
<p>{SITE.name} is open source under the MIT license.</p>
<nav class="flex flex-wrap gap-x-5 gap-y-2">
<a href="/docs/" class="hover:text-foreground">Docs</a>
<a href="/use-cases/" class="hover:text-foreground">Use cases</a>
<a href={SITE.repo} target="_blank" rel="noreferrer" class="hover:text-foreground">GitHub</a>
<a href={`${SITE.repo}/blob/main/CHANGELOG.md`} target="_blank" rel="noreferrer" class="hover:text-foreground">Changelog</a>
<a href={`${SITE.repo}/blob/main/.github/SECURITY.md`} target="_blank" rel="noreferrer" class="hover:text-foreground">Security</a>
</nav>
</div>
</footer>
130 changes: 130 additions & 0 deletions apps/site/src/components/GatewayFlow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<script setup lang="ts">
import { Bot, Code2, Database, GitBranch, Globe, Terminal } from "@lucide/vue";
import { computed, onMounted, onUnmounted, ref } from "vue";

type Client = { id: string; label: string; icon: typeof Terminal; auth: string; y: number };
type Upstream = { id: string; label: string; icon: typeof GitBranch; y: number };

const clients: Client[] = [
{ id: "claude-code", label: "Claude Code", icon: Terminal, auth: "API key", y: 30 },
{ id: "cursor", label: "Cursor", icon: Code2, auth: "API key", y: 120 },
{ id: "claude-ai", label: "claude.ai", icon: Bot, auth: "OAuth", y: 210 }
];

const upstreams: Upstream[] = [
{ id: "github", label: "GitHub", icon: GitBranch, y: 30 },
{ id: "postgres", label: "Postgres", icon: Database, y: 120 },
{ id: "remote", label: "Remote MCP", icon: Globe, y: 210 }
];

const GATEWAY = { x: 190, y: 88, w: 150, h: 64 };
const CLIENT_W = 140;
const UPSTREAM_X = 400;

const active = ref<string | null>(null);
const tick = ref(0);
let timer: ReturnType<typeof setInterval> | undefined;

onMounted(() => {
timer = setInterval(() => {
tick.value += 1;
}, 2600);
});

onUnmounted(() => {
if (timer) clearInterval(timer);
});

function inbound(client: Client): string {
const x0 = CLIENT_W;
const y0 = client.y + 20;
const x1 = GATEWAY.x;
const y1 = GATEWAY.y + GATEWAY.h / 2;
const cx = (x0 + x1) / 2;
return `M ${x0} ${y0} C ${cx} ${y0}, ${cx} ${y1}, ${x1} ${y1}`;
}

function outbound(upstream: Upstream): string {
const x0 = GATEWAY.x + GATEWAY.w;
const y0 = GATEWAY.y + GATEWAY.h / 2;
const x1 = UPSTREAM_X;
const y1 = upstream.y + 20;
const cx = (x0 + x1) / 2;
return `M ${x0} ${y0} C ${cx} ${y0}, ${cx} ${y1}, ${x1} ${y1}`;
}

const routes = computed(() =>
clients.map((client, index) => {
const upstream = upstreams[(index + tick.value) % upstreams.length]!;
return {
id: client.id,
path: `${inbound(client)} ${outbound(upstream).replace(/^M [\d.]+ [\d.]+ /, "L ")}`,
delay: `${index * 0.9}s`,
dimmed: active.value !== null && active.value !== client.id
};
})
);

const focusedClient = computed(() => clients.find((client) => client.id === active.value) ?? null);
</script>

<template>
<div class="relative w-full select-none pb-8" role="img" aria-label="Clients connect to one Junctio endpoint, which fans out to upstream MCP servers">
<svg viewBox="0 0 540 250" class="h-auto w-full overflow-visible">
<g class="stroke-border" fill="none" stroke-width="1.25">
<path v-for="client in clients" :key="`in-${client.id}`" :d="inbound(client)" />
<path v-for="upstream in upstreams" :key="`out-${upstream.id}`" :d="outbound(upstream)" />
</g>
<foreignObject
v-for="client in clients"
:key="client.id"
:x="0"
:y="client.y"
:width="CLIENT_W"
height="40"
>
<button
type="button"
class="flex h-10 w-full items-center gap-2 rounded-lg border bg-card px-3 text-left text-[13px] font-medium transition-colors hover:border-foreground/40"
:class="active === client.id ? 'border-foreground/60' : ''"
@mouseenter="active = client.id"
@mouseleave="active = null"
@focus="active = client.id"
@blur="active = null"
>
<component :is="client.icon" class="size-4 shrink-0 text-muted-foreground" />
<span class="truncate">{{ client.label }}</span>
</button>
</foreignObject>
<foreignObject :x="GATEWAY.x" :y="GATEWAY.y" :width="GATEWAY.w" :height="GATEWAY.h">
<div class="flex h-16 flex-col justify-center rounded-lg border-2 border-foreground bg-card px-3.5">
<span class="text-sm font-semibold">Junctio</span>
<span class="text-xs text-muted-foreground">
{{ focusedClient ? `${focusedClient.auth} · one endpoint` : "API key · OAuth · one endpoint" }}
</span>
</div>
</foreignObject>
<foreignObject
v-for="upstream in upstreams"
:key="upstream.id"
:x="UPSTREAM_X"
:y="upstream.y"
:width="140"
height="40"
>
<div class="flex h-10 items-center gap-2 rounded-lg border bg-card px-3 text-[13px] font-medium">
<component :is="upstream.icon" class="size-4 shrink-0 text-muted-foreground" />
<span class="truncate">{{ upstream.label }}</span>
</div>
</foreignObject>
<g v-for="route in routes" :key="route.id" class="flow-dot transition-opacity" :class="route.dimmed ? 'opacity-15' : ''">
<circle r="4" class="fill-foreground">
<animateMotion :path="route.path" dur="2.6s" repeatCount="indefinite" :begin="route.delay" />
</circle>
</g>
</svg>
<div class="pointer-events-none absolute bottom-0 left-1/2 -translate-x-1/2 rounded-full border bg-card px-2.5 py-0.5 text-[11px] text-muted-foreground">
<span class="mr-1.5 inline-block size-1.5 rounded-full bg-success align-middle"></span>upstream token refreshed 41 s before expiry
</div>
</div>
</template>
12 changes: 12 additions & 0 deletions apps/site/src/components/GithubMark.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
interface Props {
class?: string;
}
const { class: className } = Astro.props;
---

<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class={className}>
<path
d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2.17c-3.2.7-3.88-1.37-3.88-1.37-.52-1.33-1.28-1.68-1.28-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.19 1.76 1.19 1.03 1.76 2.7 1.25 3.36.96.1-.75.4-1.25.73-1.54-2.55-.29-5.24-1.28-5.24-5.68 0-1.26.45-2.28 1.19-3.09-.12-.29-.52-1.46.11-3.04 0 0 .97-.31 3.17 1.18a11 11 0 0 1 5.78 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.58.23 2.75.11 3.04.74.81 1.19 1.83 1.19 3.09 0 4.41-2.69 5.38-5.25 5.67.41.36.78 1.06.78 2.14v3.17c0 .31.21.67.8.56A11.5 11.5 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5z"
/>
</svg>
Loading
Loading