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
14 changes: 14 additions & 0 deletions apps/www/app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { revalidatePath } from "next/cache";
import { type NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
const token = request.headers.get("authorization")?.replace("Bearer ", "");

if (token !== process.env.REVALIDATION_TOKEN) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Comment thread
perkinsjr marked this conversation as resolved.
}

revalidatePath("/changelog");

return NextResponse.json({ revalidated: true });
}
94 changes: 94 additions & 0 deletions apps/www/app/changelog/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { allChangelogs } from "content-collections";

const GITHUB_REPO = "unkeyed/unkey";
const CHANGELOG_PATH = "docs/product/changelog";

function parseFrontmatter(source: string): {
title: string;
description?: string;
tags: string[];
} {
const match = source.match(/^---\n([\s\S]*?)\n---/);
if (!match) {
return { title: "", tags: ["product"] };
}

const fm = match[1];

const get = (key: string) => {
const m = fm.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
return m ? m[1].replace(/^["']|["']$/g, "").trim() : undefined;
};

const tags = ["product"];

return { title: get("title") ?? "", description: get("description"), tags };
}

async function fetchProductChangelogs() {
try {
const headers: HeadersInit = { Accept: "application/vnd.github.v3+json" };
if (process.env.GITHUB_TOKEN) {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}

const res = await fetch(
`https://api.github.com/repos/${GITHUB_REPO}/contents/${CHANGELOG_PATH}`,
{ headers, next: { revalidate: 86400 } },
);
if (!res.ok) {
return [];
}

const entries = (await res.json()) as Array<{
type: string;
name: string;
download_url: string;
}>;

const mdxFiles = entries.filter((f) => f.type === "file" && f.name.endsWith(".mdx"));

const results = await Promise.all(
mdxFiles.map(async (file) => {
try {
const raw = await fetch(file.download_url, {
next: { revalidate: 86400 },
});
if (!raw.ok) {
console.error(`Failed to fetch changelog file ${file.name}: ${raw.status}`);
return null;
}
const source = (await raw.text()).replace(/^noindex:\s*.+$/m, "");
const date = file.name.slice(0, -4); // YYYY-MM-DD from filename
const { title, description, tags } = parseFrontmatter(source);
return { slug: date, date, title, description, tags, source };
} catch (err) {
console.error(`Failed to process changelog file ${file.name}:`, err);
return null;
}
}),
);

return results.filter((entry) => entry !== null);
} catch (err) {
console.error("Failed to fetch product changelogs:", err);
return [];
}
}

export async function getAllChangelogs() {
const productEntries = await fetchProductChangelogs();

const collectionEntries = allChangelogs.map((e) => ({
...e,
_kind: "collection" as const,
}));
const githubEntries = productEntries.map((e) => ({
...e,
_kind: "github" as const,
}));

return [...collectionEntries, ...githubEntries].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
);
}
26 changes: 18 additions & 8 deletions apps/www/app/changelog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { RainbowDarkButton } from "@/components/button";
import { CTA } from "@/components/cta";

import { ChangelogGridItem } from "@/components/changelog/changelog-grid-item";
import { changelogMdxComponents } from "@/components/changelog/changelog-mdx-components";
import { SideList } from "@/components/changelog/side-list";
import { CTA } from "@/components/cta";
import { MDX } from "@/components/mdx-content";
import { ChangelogLight } from "@/components/svg/changelog";
import { allChangelogs } from "content-collections";
import { formatDate } from "date-fns";
import { ArrowRight } from "lucide-react";
import { MDXRemote } from "next-mdx-remote/rsc";
import { getAllChangelogs } from "./data";

export default async function Changelogs() {
const changelogs = allChangelogs.sort((a, b) => {
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
const changelogs = await getAllChangelogs();

return (
<>
Expand Down Expand Up @@ -48,8 +48,18 @@ export default async function Changelogs() {
</div>
</div>
<div className="flex flex-col w-full sm:overflow-hidden">
{changelogs?.map((changelog) => (
<ChangelogGridItem key={changelog.title} changelog={changelog} />
{changelogs.map((entry) => (
<ChangelogGridItem key={entry.slug} changelog={entry}>
{entry._kind === "collection" ? (
<MDX code={entry.mdx} />
) : (
<MDXRemote
source={entry.source}
options={{ parseFrontmatter: true }}
components={changelogMdxComponents}
/>
)}
</ChangelogGridItem>
))}
</div>
</div>
Expand Down
8 changes: 5 additions & 3 deletions apps/www/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { allChangelogs, allGlossaries, allPolicies, allPosts } from "content-collections";
import type { Changelog, Glossary, Policy, Post } from "content-collections";
import { getAllChangelogs } from "@/app/changelog/data";
import { allGlossaries, allPolicies, allPosts } from "content-collections";
import type { Glossary, Policy, Post } from "content-collections";
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = "https://www.unkey.com";
Expand All @@ -13,7 +14,8 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
url: `${baseUrl}/policies/${policy.slug}`,
}));

const changelogs: MetadataRoute.Sitemap = allChangelogs.map((changelog: Changelog) => ({
const allChangelogEntries = await getAllChangelogs();
const changelogs: MetadataRoute.Sitemap = allChangelogEntries.map((changelog) => ({
url: `${baseUrl}/changelog#${changelog.slug}`,
lastModified: changelog.date,
}));
Expand Down
4 changes: 2 additions & 2 deletions apps/www/app/templates/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRight, SearchX, VenetianMask } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { type Resolver, useForm } from "react-hook-form";
import { type Framework, type Language, templates } from "./data";

export function TemplatesClient() {
const form = useForm<TemplatesFormValues>({
resolver: zodResolver(schema),
resolver: zodResolver(schema) as Resolver<TemplatesFormValues>,
defaultValues: getDefaulTemplatesFormValues(),
reValidateMode: "onChange",
});
Expand Down
193 changes: 193 additions & 0 deletions apps/www/components/changelog/callouts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import type { ReactNode } from "react";

type CalloutProps = {
children: ReactNode;
title?: string;
};

const variants = {
info: {
border: "border-l-[#3DC5FA]",
bg: "bg-[#3DC5FA]/10",
titleColor: "text-[#3DC5FA]",
icon: (
<svg
width="16"
height="16"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0 mt-0.5"
>
<path
d="M16.5 16.5V19.5"
stroke="#3DC5FA"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.5 14.25C16.9142 14.25 17.25 13.9142 17.25 13.5C17.25 13.0858 16.9142 12.75 16.5 12.75C16.0858 12.75 15.75 13.0858 15.75 13.5C15.75 13.9142 16.0858 14.25 16.5 14.25Z"
fill="#3DC5FA"
/>
<path
d="M16.5 24.5C20.9183 24.5 24.5 20.9183 24.5 16.5C24.5 12.0817 20.9183 8.5 16.5 8.5C12.0817 8.5 8.5 12.0817 8.5 16.5C8.5 20.9183 12.0817 24.5 16.5 24.5Z"
stroke="#3DC5FA"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
success: {
border: "border-l-[#3CEEAE]",
bg: "bg-[#3CEEAE]/10",
titleColor: "text-[#3CEEAE]",
icon: (
<svg
width="16"
height="16"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0 mt-0.5"
>
<path
d="M13.5 16.5L15.5 18.5L19.5 14.5"
stroke="#3CEEAE"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.5 24.5C20.9183 24.5 24.5 20.9183 24.5 16.5C24.5 12.0817 20.9183 8.5 16.5 8.5C12.0817 8.5 8.5 12.0817 8.5 16.5C8.5 20.9183 12.0817 24.5 16.5 24.5Z"
stroke="#3CEEAE"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
warning: {
border: "border-l-[#FFD55D]",
bg: "bg-[#FFD55D]/10",
titleColor: "text-[#FFD55D]",
icon: (
<svg
width="16"
height="16"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0 mt-0.5"
>
<path
d="M16.5 13.5V16.5"
stroke="#FFD55D"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.5 20.25C16.9142 20.25 17.25 19.9142 17.25 19.5C17.25 19.0858 16.9142 18.75 16.5 18.75C16.0858 18.75 15.75 19.0858 15.75 19.5C15.75 19.9142 16.0858 20.25 16.5 20.25Z"
fill="#FFD55D"
/>
<path
d="M25.2918 21.1356L17.9961 8.36824C17.6893 7.83134 17.1184 7.5 16.5 7.5C15.8816 7.5 15.3107 7.83134 15.0039 8.36824L7.70822 21.1356C7.57177 21.3744 7.5 21.6447 7.5 21.9197C7.5 22.7925 8.20753 23.5 9.08032 23.5H23.9197C24.7925 23.5 25.5 22.7925 25.5 21.9197C25.5 21.6447 25.4282 21.3744 25.2918 21.1356Z"
stroke="#FFD55D"
strokeWidth="2"
strokeLinejoin="round"
/>
</svg>
),
},
error: {
border: "border-l-[#FB1048]",
bg: "bg-[#FB1048]/10",
titleColor: "text-[#FB1048]",
icon: (
<svg
width="16"
height="16"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0 mt-0.5"
>
<path
d="M16.5 12.5V15.5"
stroke="#FB1048"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.5 19.25C16.9142 19.25 17.25 18.9142 17.25 18.5C17.25 18.0858 16.9142 17.75 16.5 17.75C16.0858 17.75 15.75 18.0858 15.75 18.5C15.75 18.9142 16.0858 19.25 16.5 19.25Z"
fill="#FB1048"
/>
<path
d="M24.8251 14.8251L18.2071 8.20711C17.7544 7.75435 17.1403 7.5 16.5 7.5C15.8597 7.5 15.2456 7.75435 14.7929 8.20711L8.17488 14.8251C7.74276 15.2572 7.5 15.8433 7.5 16.4544C7.5 17.1189 7.78683 17.751 8.28687 18.1885L14.7474 23.8415C15.2326 24.266 15.8553 24.5 16.5 24.5C17.1447 24.5 17.7674 24.266 18.2526 23.8415L24.7131 18.1885C25.2132 17.751 25.5 17.1189 25.5 16.4544C25.5 15.8433 25.2572 15.2572 24.8251 14.8251Z"
stroke="#FB1048"
strokeWidth="2"
strokeLinejoin="round"
/>
</svg>
),
},
} as const;

type VariantKey = keyof typeof variants;

function Callout({ children, title, variant }: CalloutProps & { variant: VariantKey }) {
const v = variants[variant];
return (
<div
className={`flex gap-3 border-l-4 ${v.border} ${v.bg} rounded-r-md px-4 py-3 my-4
[&_p]:!text-sm [&_p]:!leading-6 [&_p]:!text-white/80 [&_p]:!m-0`}
>
<span className="mt-[3px]">{v.icon}</span>
<div className="min-w-0">
{title && <p className={`!font-semibold !text-sm !mb-1 ${v.titleColor}`}>{title}</p>}
{children}
</div>
</div>
);
}

export const Note = ({ children, title }: CalloutProps) => (
<Callout variant="info" title={title}>
{children}
</Callout>
);

export const Info = ({ children, title }: CalloutProps) => (
<Callout variant="info" title={title}>
{children}
</Callout>
);

export const Tip = ({ children, title }: CalloutProps) => (
<Callout variant="success" title={title}>
{children}
</Callout>
);

export const Check = ({ children, title }: CalloutProps) => (
<Callout variant="success" title={title}>
{children}
</Callout>
);

export const Warning = ({ children, title }: CalloutProps) => (
<Callout variant="warning" title={title}>
{children}
</Callout>
);

export const Danger = ({ children, title }: CalloutProps) => (
<Callout variant="error" title={title}>
{children}
</Callout>
);
Loading
Loading