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
4 changes: 2 additions & 2 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1245,7 +1245,7 @@ Homepage: https://github.com/honojs/node-server

Copyright (c) 2022 - present, Yusuke Wada and Hono contributors

### `@hookform/resolvers@5.2.2`
### `@hookform/resolvers@5.4.0`
Homepage: https://react-hook-form.com

Copyright (c) 2019-present Beier(Bill) Luo
Expand Down Expand Up @@ -4880,7 +4880,7 @@ Homepage: https://github.com/vasturiano/react-force-graph

_(No LICENSE file in package; SPDX identifier in `package.json` is the sole declared grant.)_

### `react-hook-form@7.74.0`
### `react-hook-form@7.80.0`
Homepage: https://react-hook-form.com

Copyright (c) 2019-present Beier(Bill) Luo
Expand Down
18 changes: 16 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions docs/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
NEXT_PUBLIC_INKEEP_API_KEY=""
NEXT_PUBLIC_POSTHOG_KEY=""
NEXT_PUBLIC_POSTHOG_HOST="https://us.posthog.com"

# Server-only. Powers POST /api/subscribe (product-update signups via Resend).
# Never prefix with NEXT_PUBLIC_ — these must not reach the client bundle.
RESEND_API_KEY=""
RESEND_SEGMENT_ID=""
4 changes: 4 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.41.0",
"@floating-ui/dom": "^1.7.6",
"@hookform/resolvers": "^5.4.0",
"@inkeep/cxkit-react": "^0.5.117",
"@inkeep/open-knowledge-core": "workspace:*",
"@radix-ui/react-popover": "^1.1.17",
Expand All @@ -32,6 +33,7 @@
"@tiptap/react": "^3.22.3",
"@tiptap/suggestion": "^3.22.3",
"@types/mdx": "^2.0.13",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "~16.1.0",
"fumadocs-mdx": "~14.0.3",
Expand All @@ -44,6 +46,8 @@
"radix-ui": "^1.4.3",
"react": "^19",
"react-dom": "^19",
"react-hook-form": "^7.80.0",
"resend": "^6.16.0",
"tailwind-merge": "^3.6.0",
"zod": "^4.3.6"
},
Expand Down
10 changes: 8 additions & 2 deletions docs/src/app/(home)/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { DiscordIcon } from '@/components/icons/discord';
import { GitHubIcon } from '@/components/icons/github';
import { XIcon } from '@/components/icons/x';
import { InkeepLogo } from '@/components/inkeep-logo';
import { SubscribeForm } from '@/components/subscribe-form';
import { DotTexture } from './dot-texture';

const socialLinks = [
{ href: 'https://github.com/inkeep/open-knowledge', label: 'GitHub', Icon: GitHubIcon },
Expand All @@ -17,8 +19,12 @@ const legalLinks = [

export function SiteFooter() {
return (
<footer className="px-6 py-10">
<div className="container mx-auto grid grid-cols-1 items-center gap-6 min-[24rem]:grid-cols-[auto_auto] min-[24rem]:justify-between sm:grid-cols-3 sm:justify-normal">
<footer className="relative space-y-16 overflow-hidden px-6 py-10">
<DotTexture variant="left" className="bottom-0 left-0 w-32 sm:w-60 lg:w-96" />
<div className="container relative z-10 mx-auto">
<SubscribeForm />
</div>
<div className="container relative z-10 mx-auto mt-8 grid grid-cols-1 items-center gap-6 min-[24rem]:grid-cols-[auto_auto] min-[24rem]:justify-between sm:grid-cols-3 sm:justify-normal">
<div className="flex items-center justify-center gap-5 min-[24rem]:justify-start">
{socialLinks.map(({ href, label, Icon }) => (
<Link
Expand Down
48 changes: 48 additions & 0 deletions docs/src/app/api/subscribe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextResponse } from 'next/server';
import { Resend } from 'resend';
import { z } from 'zod';

export const dynamic = 'force-dynamic';

const RESEND_API_KEY = process.env.RESEND_API_KEY;
const RESEND_SEGMENT_ID = process.env.RESEND_SEGMENT_ID;

const subscribeSchema = z.object({
email: z.email(),
});

export async function POST(request: Request): Promise<NextResponse> {
if (!RESEND_API_KEY || !RESEND_SEGMENT_ID) {
console.error('[subscribe] RESEND_API_KEY or RESEND_SEGMENT_ID is not configured');
return NextResponse.json(
{ error: 'Subscriptions are not available right now.' },
{ status: 503 },
);
}

let payload: unknown;
try {
payload = await request.json();
} catch {
return NextResponse.json({ error: 'Enter a valid email address.' }, { status: 400 });
}

const parsed = subscribeSchema.safeParse(payload);
if (!parsed.success) {
return NextResponse.json({ error: 'Enter a valid email address.' }, { status: 400 });
}

const resend = new Resend(RESEND_API_KEY);
const { error } = await resend.contacts.create({
email: parsed.data.email,
unsubscribed: false,
segments: [{ id: RESEND_SEGMENT_ID }],
});

if (error) {
console.error(`[subscribe] Resend create-contact failed: ${error.name} - ${error.message}`);
return NextResponse.json({ error: 'Something went wrong. Please try again.' }, { status: 502 });
}

return NextResponse.json({ ok: true });
}
101 changes: 101 additions & 0 deletions docs/src/components/subscribe-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use client';

import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';
import { MarketingButton } from '@/app/(home)/marketing-button';
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
import { Input } from '@/components/ui/input';

const subscribeSchema = z.object({
email: z.email({ message: 'Please enter a valid email address.' }),
});

type SubscribeValues = z.infer<typeof subscribeSchema>;

export function SubscribeForm() {
const [submitFailed, setSubmitFailed] = useState(false);
const [subscribed, setSubscribed] = useState(false);

const form = useForm<SubscribeValues>({
resolver: zodResolver(subscribeSchema),
defaultValues: { email: '' },
});

async function onSubmit(values: SubscribeValues) {
setSubmitFailed(false);
try {
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(values),
});
if (response.ok) {
form.reset();
setSubscribed(true);
} else {
setSubmitFailed(true);
}
} catch (err) {
console.error(
`[subscribe-form] fetch failed: ${err instanceof Error ? err.message : String(err)}`,
);
setSubmitFailed(true);
}
}

if (subscribed) {
return (
<p className="text-sm text-slide-muted" role="status">
Thanks for subscribing. Watch your inbox for product updates.
</p>
);
}

return (
<form onSubmit={form.handleSubmit(onSubmit)} className="flex w-full max-w-2xl flex-col gap-2">
<Controller
control={form.control}
name="email"
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel className="text-lg leading-tight tracking-tight" htmlFor={field.name}>
Stay in the loop
</FieldLabel>
<div className="relative h-14 w-full">
<Input
{...field}
id={field.name}
type="email"
inputMode="email"
autoComplete="email"
spellCheck={false}
placeholder="my@email.com"
aria-invalid={fieldState.invalid}
className="h-full w-full rounded-full border bg-fd-background pr-36 pl-6 text-gray-900 placeholder:text-slide-muted/60 focus-visible:ring-2 focus-visible:ring-slide-accent focus-visible:ring-offset-2 shadow-none"
/>
<div className="absolute inset-y-0 right-2 flex items-center">
<MarketingButton
type="submit"
variant="primary"
size="md"
disabled={form.formState.isSubmitting}
className="h-10"
>
Subscribe
</MarketingButton>
</div>
</div>
<FieldError className="text-red-500" errors={[fieldState.error]} />
</Field>
)}
/>
{submitFailed ? (
<p className="text-sm text-red-500" role="alert">
Something went wrong. Please try again.
</p>
) : null}
</form>
);
}
Loading