From e50d9f86b1e0e7165897c6026f8b57719d060481 Mon Sep 17 00:00:00 2001 From: "Dominik K." Date: Fri, 13 Mar 2026 00:28:57 +0100 Subject: [PATCH 1/4] feat: add changelog page powered by Notra - Add changelog page with split-view layout (sticky left sidebar, scrollable right content) - Integrate Notra REST API for fetching changelog posts - Add 'Powered by Notra' badge with SVG logo linking to usenotra.com - Add changelog links to main and docs navbars - Fix JSX attribute casing in icon components (clipRule, fillRule) - Bump Next.js to ^16.1.6 - External links in changelog content open in new tabs --- apps/docs/app/(home)/changelog/page.tsx | 226 ++++++++++++++++++++++++ apps/docs/components/docs-navbar.tsx | 4 + apps/docs/components/icons/ccpa.tsx | 4 +- apps/docs/components/icons/hipaa.tsx | 2 +- apps/docs/components/icons/iso27001.tsx | 6 +- apps/docs/components/navbar.tsx | 4 + apps/docs/lib/changelog-query.ts | 93 ++++++++++ apps/docs/lib/sitemap-generator.ts | 5 + apps/docs/package.json | 2 +- bun.lock | 26 ++- 10 files changed, 364 insertions(+), 8 deletions(-) create mode 100644 apps/docs/app/(home)/changelog/page.tsx create mode 100644 apps/docs/lib/changelog-query.ts diff --git a/apps/docs/app/(home)/changelog/page.tsx b/apps/docs/app/(home)/changelog/page.tsx new file mode 100644 index 000000000..8380be95a --- /dev/null +++ b/apps/docs/app/(home)/changelog/page.tsx @@ -0,0 +1,226 @@ +import { + RocketLaunchIcon, +} from "@phosphor-icons/react/ssr"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { Footer } from "@/components/footer"; +import Section from "@/components/landing/section"; +import { Spotlight } from "@/components/landing/spotlight"; +import { Prose } from "@/components/prose"; +import { StructuredData } from "@/components/structured-data"; +import type { NotraPost } from "@/lib/changelog-query"; +import { getChangelogs } from "@/lib/changelog-query"; + +export const revalidate = 3600; + +export const metadata: Metadata = { + title: "Changelog | Databuddy", + description: + "Stay up to date with the latest features, improvements, and fixes shipped to Databuddy.", + alternates: { + canonical: "https://www.databuddy.cc/changelog", + }, + openGraph: { + title: "Changelog | Databuddy", + description: + "Stay up to date with the latest features, improvements, and fixes shipped to Databuddy.", + url: "https://www.databuddy.cc/changelog", + images: ["/og-image.png"], + }, +}; + +function formatDate(date: string) { + return new Date(date).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +function externalizeLinks(html: string): string { + return html.replace( + /"); + if (h2Index === -1) { + return { description: html, body: "" }; + } + return { + description: html.slice(0, h2Index).trim(), + body: html.slice(h2Index).trim(), + }; +} + +function ChangelogEntry({ + post, + isLast, +}: { post: NotraPost; isLast: boolean }) { + const { description, body } = splitContent(externalizeLinks(post.content)); + + return ( +
+
+
+ +

+ {post.title} +

+ {description && ( + + )} +
+
+ +
+ {body ? ( + + ) : ( +

+ No additional details. +

+ )} +
+
+ ); +} + +export default async function ChangelogPage() { + const result = await getChangelogs(); + const posts = "error" in result ? [] : result.posts; + + return ( +
+ + + +
+
+
+
+
+
+

+ What's new in Databuddy +

+

+ All the latest features, improvements, and fixes shipped + to Databuddy — straight from the team. +

+
+
+
+
+ +
+
+
+ {posts.length > 0 ? ( +
+ {posts.map((post, i) => ( + + ))} +
+ ) : ( +
+
+ +

+ No releases yet +

+

+ We're working hard on new features. Check back + soon for the latest updates. +

+
+
+ )} +
+
+ + Powered by + + Notra + +
+
+ +
+
+
+ +
+ +
+
+
+
+ ); +} diff --git a/apps/docs/components/docs-navbar.tsx b/apps/docs/components/docs-navbar.tsx index 7d7e6eca1..8458aa552 100644 --- a/apps/docs/components/docs-navbar.tsx +++ b/apps/docs/components/docs-navbar.tsx @@ -300,6 +300,10 @@ export const navMenu = [ name: "Blog", path: "/blog", }, + { + name: "Changelog", + path: "/changelog", + }, { name: "Pricing", path: "/pricing", diff --git a/apps/docs/components/icons/ccpa.tsx b/apps/docs/components/icons/ccpa.tsx index 7333ce26d..03a90f102 100644 --- a/apps/docs/components/icons/ccpa.tsx +++ b/apps/docs/components/icons/ccpa.tsx @@ -13,10 +13,10 @@ export function CCPAIcon(props: SVGProps) { > CCPA ) { > HIPAA ) { > ISO 27001 ) { fill="currentColor" /> ) { fill="currentColor" /> (endpoint: string): Promise { + try { + const apiKey = process.env.NOTRA_API_KEY; + const orgId = process.env.NOTRA_ORGANIZATION_ID; + + if (!(apiKey && orgId)) { + return { + error: true, + status: 500, + statusText: + "NOTRA_API_KEY and NOTRA_ORGANIZATION_ID environment variables are required", + }; + } + + const response = await fetch( + `https://api.usenotra.com/v1/${orgId}/${endpoint}`, + { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + next: { revalidate: 3600 }, + } + ); + + if (!response.ok) { + return { + error: true, + status: response.status, + statusText: response.statusText, + }; + } + + return (await response.json()) as T; + } catch (error) { + console.error(`Error fetching ${endpoint} from Notra:`, error); + return { + error: true, + status: 500, + statusText: "Internal Error", + }; + } +} + +export const getChangelogs = cache((page = 1, limit = 100) => + fetchFromNotra( + `posts?status=published&sort=desc&page=${page}&limit=${limit}` + ) +); + +export const getChangelogPost = cache((postId: string) => + fetchFromNotra(`posts/${postId}`) +); diff --git a/apps/docs/lib/sitemap-generator.ts b/apps/docs/lib/sitemap-generator.ts index fda9cfea5..0e36b718b 100644 --- a/apps/docs/lib/sitemap-generator.ts +++ b/apps/docs/lib/sitemap-generator.ts @@ -24,6 +24,7 @@ const priorityRules = [ { pattern: "/pricing", priority: 0.8 }, { pattern: "/Integrations/", priority: 0.7 }, { pattern: "/blog", priority: 0.7 }, + { pattern: "/changelog", priority: 0.7 }, { pattern: "/api", priority: 0.7 }, { pattern: "/roadmap", priority: 0.6 }, { pattern: "/sponsors", priority: 0.6 }, @@ -70,6 +71,9 @@ function getChangeFrequency(url: string): "weekly" | "monthly" | "yearly" { if (url.includes("/blog") && url !== "/blog") { return "monthly"; } + if (url.includes("/changelog")) { + return "weekly"; + } if (url.includes("/pricing") || url.includes("/roadmap")) { return "monthly"; } @@ -104,6 +108,7 @@ export async function generateSitemapEntries(): Promise { "/privacy", "/llms.txt", "/api", + "/changelog", "/contributors", "/pricing", "/roadmap", diff --git a/apps/docs/package.json b/apps/docs/package.json index 1cfbbcd3e..0d9d19464 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -61,7 +61,7 @@ "libphonenumber-js": "^1.12.38", "lucide-react": "^0.539.0", "motion": "^12.23.26", - "next": "^16.0.9", + "next": "^16.1.6", "next-themes": "^0.4.6", "nuqs": "^2.8.5", "ogl": "^1.0.11", diff --git a/bun.lock b/bun.lock index 7fea5a6f8..9adf330c5 100644 --- a/bun.lock +++ b/bun.lock @@ -276,7 +276,7 @@ "libphonenumber-js": "^1.12.38", "lucide-react": "^0.539.0", "motion": "^12.23.26", - "next": "^16.0.9", + "next": "^16.1.6", "next-themes": "^0.4.6", "nuqs": "^2.8.5", "ogl": "^1.0.11", @@ -3876,6 +3876,8 @@ "@databuddy/docs/lucide-react": ["lucide-react@0.539.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg=="], + "@databuddy/docs/next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="], + "@databuddy/env/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], "@databuddy/links/@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.210.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/otlp-exporter-base": "0.210.0", "@opentelemetry/otlp-transformer": "0.210.0", "@opentelemetry/resources": "2.4.0", "@opentelemetry/sdk-trace-base": "2.4.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-qVUY7Hsm/t5buGOtPcTV1Ch4W9kj2wGaQaAF5FO4XR8TMKl2GM45tUCnr0/1dF3wo4RG9khMxrddeQWdRL4fIg=="], @@ -4712,6 +4714,26 @@ "@databuddy/docs/babel-plugin-react-compiler/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@databuddy/docs/next/@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="], + + "@databuddy/docs/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="], + + "@databuddy/docs/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="], + + "@databuddy/docs/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="], + + "@databuddy/docs/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="], + + "@databuddy/docs/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="], + + "@databuddy/docs/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="], + + "@databuddy/docs/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="], + + "@databuddy/docs/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="], + + "@databuddy/docs/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "@databuddy/env/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "@databuddy/links/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.4.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw=="], @@ -5384,6 +5406,8 @@ "@databuddy/dashboard/ultracite/trpc-cli/zod-to-json-schema": ["zod-to-json-schema@3.25.0", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ=="], + "@databuddy/docs/next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "@databuddy/links/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.38.0", "", {}, "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg=="], "@databuddy/links/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@8.0.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw=="], From a01a51a914fa06b4903a341290cb0d86c8063ec4 Mon Sep 17 00:00:00 2001 From: "Dominik K." Date: Fri, 13 Mar 2026 00:28:57 +0100 Subject: [PATCH 2/4] feat: add changelog page powered by Notra - Add changelog page with split-view layout (sticky left sidebar, scrollable right content) - Integrate Notra REST API for fetching changelog posts - Add 'Powered by Notra' badge with SVG logo linking to usenotra.com - Add changelog links to main and docs navbars - Fix JSX attribute casing in icon components (clipRule, fillRule) - Bump Next.js to ^16.1.6 - External links in changelog content open in new tabs --- .env.example | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.env.example b/.env.example index 2aa0f9d29..48a0ff4ac 100644 --- a/.env.example +++ b/.env.example @@ -44,5 +44,9 @@ NEXT_PUBLIC_API_URL="http://localhost:3001" MARBLE_WORKSPACE_KEY= MARBLE_API_URL=https://api.marblecms.com +# Not Necessary unless using changelog +NOTRA_API_KEY= +NOTRA_ORGANIZATION_ID= + UPSTASH_QSTASH_TOKEN="i love you upstash" UPSTASH_QSTASH_TOKEN=test-upstash-qstash-token From b64a35556266529efdabc528b9810e3ec1b58c39 Mon Sep 17 00:00:00 2001 From: "Dominik K." Date: Fri, 13 Mar 2026 00:38:29 +0100 Subject: [PATCH 3/4] changelog: add twitter meta, extract Notra logo to public SVG, copy fixes Made-with: Cursor --- apps/docs/app/(home)/changelog/page.tsx | 43 ++++++++++--------------- apps/docs/public/notra.svg | 5 +++ 2 files changed, 22 insertions(+), 26 deletions(-) create mode 100644 apps/docs/public/notra.svg diff --git a/apps/docs/app/(home)/changelog/page.tsx b/apps/docs/app/(home)/changelog/page.tsx index 8380be95a..d9ed3a697 100644 --- a/apps/docs/app/(home)/changelog/page.tsx +++ b/apps/docs/app/(home)/changelog/page.tsx @@ -1,7 +1,6 @@ -import { - RocketLaunchIcon, -} from "@phosphor-icons/react/ssr"; +import { RocketLaunchIcon } from "@phosphor-icons/react/ssr"; import type { Metadata } from "next"; +import Image from "next/image"; import Link from "next/link"; import { Footer } from "@/components/footer"; import Section from "@/components/landing/section"; @@ -27,6 +26,13 @@ export const metadata: Metadata = { url: "https://www.databuddy.cc/changelog", images: ["/og-image.png"], }, + twitter: { + card: "summary_large_image", + title: "Changelog | Databuddy", + description: + "Stay up to date with the latest features, improvements, and fixes shipped to Databuddy.", + images: ["/og-image.png"], + }, }; function formatDate(date: string) { @@ -134,7 +140,7 @@ export default async function ChangelogPage() {

All the latest features, improvements, and fixes shipped - to Databuddy — straight from the team. + to Databuddy, straight from the team.

@@ -184,29 +190,14 @@ export default async function ChangelogPage() { className="mx-auto flex w-fit items-center gap-2 rounded-full border border-border/40 bg-card/30 px-4 py-2 text-muted-foreground/50 transition-colors hover:border-border/60 hover:text-muted-foreground/70" > Powered by - + aria-hidden + /> Notra diff --git a/apps/docs/public/notra.svg b/apps/docs/public/notra.svg new file mode 100644 index 000000000..ad9a7898e --- /dev/null +++ b/apps/docs/public/notra.svg @@ -0,0 +1,5 @@ + + Notra Logo + + + From 89111b3b6985051fcd9bee0ce6f595534aa0e364 Mon Sep 17 00:00:00 2001 From: "Dominik K." Date: Fri, 13 Mar 2026 00:40:24 +0100 Subject: [PATCH 4/4] changelog: extract format, links, split logic to lib/changelog-utils Made-with: Cursor --- apps/docs/app/(home)/changelog/page.tsx | 74 ++++++++++--------------- apps/docs/lib/changelog-utils.ts | 28 ++++++++++ 2 files changed, 57 insertions(+), 45 deletions(-) create mode 100644 apps/docs/lib/changelog-utils.ts diff --git a/apps/docs/app/(home)/changelog/page.tsx b/apps/docs/app/(home)/changelog/page.tsx index d9ed3a697..f7d69e26d 100644 --- a/apps/docs/app/(home)/changelog/page.tsx +++ b/apps/docs/app/(home)/changelog/page.tsx @@ -9,6 +9,11 @@ import { Prose } from "@/components/prose"; import { StructuredData } from "@/components/structured-data"; import type { NotraPost } from "@/lib/changelog-query"; import { getChangelogs } from "@/lib/changelog-query"; +import { + externalizeLinks, + formatChangelogDate, + splitChangelogContent, +} from "@/lib/changelog-utils"; export const revalidate = 3600; @@ -35,56 +40,35 @@ export const metadata: Metadata = { }, }; -function formatDate(date: string) { - return new Date(date).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); -} - -function externalizeLinks(html: string): string { - return html.replace( - /"); - if (h2Index === -1) { - return { description: html, body: "" }; - } - return { - description: html.slice(0, h2Index).trim(), - body: html.slice(h2Index).trim(), - }; -} - function ChangelogEntry({ post, isLast, -}: { post: NotraPost; isLast: boolean }) { - const { description, body } = splitContent(externalizeLinks(post.content)); +}: { + post: NotraPost; + isLast: boolean; +}) { + const { description, body } = splitChangelogContent( + externalizeLinks(post.content) + ); return (
-
+
-

+

{post.title}

{description && ( )}
@@ -93,8 +77,8 @@ function ChangelogEntry({
{body ? ( ) : (

@@ -139,8 +123,8 @@ export default async function ChangelogPage() { What's new in Databuddy

- All the latest features, improvements, and fixes shipped - to Databuddy, straight from the team. + All the latest features, improvements, and fixes shipped to + Databuddy, straight from the team.

@@ -158,9 +142,9 @@ export default async function ChangelogPage() {
{posts.map((post, i) => ( ))}
@@ -175,8 +159,8 @@ export default async function ChangelogPage() { No releases yet

- We're working hard on new features. Check back - soon for the latest updates. + We're working hard on new features. Check back soon for + the latest updates.

@@ -184,19 +168,19 @@ export default async function ChangelogPage() {
Powered by Notra diff --git a/apps/docs/lib/changelog-utils.ts b/apps/docs/lib/changelog-utils.ts new file mode 100644 index 000000000..21b8a0e1b --- /dev/null +++ b/apps/docs/lib/changelog-utils.ts @@ -0,0 +1,28 @@ +export function formatChangelogDate(date: string) { + return new Date(date).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +export function externalizeLinks(html: string): string { + return html.replace( + /"); + if (h2Index === -1) { + return { description: html, body: "" }; + } + return { + description: html.slice(0, h2Index).trim(), + body: html.slice(h2Index).trim(), + }; +}