diff --git a/apps/desktop/src/changelog/index.tsx b/apps/desktop/src/changelog/index.tsx
index 0ccdb7802d..e2b3d60d7f 100644
--- a/apps/desktop/src/changelog/index.tsx
+++ b/apps/desktop/src/changelog/index.tsx
@@ -154,7 +154,7 @@ function ChangelogHeader({
date: string | null;
}) {
const formattedDate = date ? safeFormat(date, "MMM d, yyyy") : null;
- const webUrl = `https://char.com/changelog/${version}`;
+ const webUrl = `https://anarlog.so/changelog/${version}`;
return (
diff --git a/apps/web/src/components/site-footer.tsx b/apps/web/src/components/site-footer.tsx
index 0e0a0f9824..417960e0fc 100644
--- a/apps/web/src/components/site-footer.tsx
+++ b/apps/web/src/components/site-footer.tsx
@@ -14,6 +14,9 @@ export function SiteFooter() {
Blog
+
+ Changelog
+
Contact
diff --git a/apps/web/src/lib/changelog.ts b/apps/web/src/lib/changelog.ts
new file mode 100644
index 0000000000..0844e2efa8
--- /dev/null
+++ b/apps/web/src/lib/changelog.ts
@@ -0,0 +1,60 @@
+import { processContent } from "@hypr/changelog";
+
+const rawEntries = import.meta.glob(
+ "../../../../packages/changelog/content/*.md",
+ {
+ eager: true,
+ import: "default",
+ query: "?raw",
+ },
+) as Record
;
+
+export const changelogEntries = Object.entries(rawEntries)
+ .map(([filePath, raw]) => {
+ const version = filePath.split("/").pop()?.replace(/\.md$/, "") ?? "";
+ const { content, date } = processContent(raw);
+
+ return {
+ version,
+ content,
+ date: normalizeDate(date),
+ };
+ })
+ .filter((entry) => entry.version)
+ .sort((a, b) => compareVersionsDesc(a.version, b.version));
+
+export function getChangelogEntry(version: string) {
+ return changelogEntries.find((entry) => entry.version === version);
+}
+
+function normalizeDate(date: string | null) {
+ return date?.replace(/^["']|["']$/g, "") ?? null;
+}
+
+function compareVersionsDesc(a: string, b: string) {
+ const left = a.split(".").map(Number);
+ const right = b.split(".").map(Number);
+ const length = Math.max(left.length, right.length);
+
+ for (let i = 0; i < length; i++) {
+ const diff = (right[i] ?? 0) - (left[i] ?? 0);
+ if (diff !== 0) return diff;
+ }
+
+ return b.localeCompare(a);
+}
+
+export function formatChangelogDate(date: string) {
+ const parsed = new Date(`${date}T00:00:00Z`);
+
+ if (Number.isNaN(parsed.getTime())) {
+ return date;
+ }
+
+ return new Intl.DateTimeFormat("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ timeZone: "UTC",
+ }).format(parsed);
+}
diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts
index 648f816eba..558551987d 100644
--- a/apps/web/src/routeTree.gen.ts
+++ b/apps/web/src/routeTree.gen.ts
@@ -15,8 +15,10 @@ import { Route as FoundersRouteImport } from './routes/founders'
import { Route as AuthRouteImport } from './routes/auth'
import { Route as ViewRouteRouteImport } from './routes/_view/route'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as ChangelogIndexRouteImport } from './routes/changelog/index'
import { Route as BlogIndexRouteImport } from './routes/blog/index'
import { Route as LegalSlugRouteImport } from './routes/legal/$slug'
+import { Route as ChangelogVersionRouteImport } from './routes/changelog/$version'
import { Route as BlogSlugRouteImport } from './routes/blog/$slug'
import { Route as ApiTemplatesRouteImport } from './routes/api/templates'
import { Route as ApiShortcutsRouteImport } from './routes/api/shortcuts'
@@ -103,6 +105,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const ChangelogIndexRoute = ChangelogIndexRouteImport.update({
+ id: '/changelog/',
+ path: '/changelog/',
+ getParentRoute: () => rootRouteImport,
+} as any)
const BlogIndexRoute = BlogIndexRouteImport.update({
id: '/blog/',
path: '/blog/',
@@ -113,6 +120,11 @@ const LegalSlugRoute = LegalSlugRouteImport.update({
path: '/legal/$slug',
getParentRoute: () => rootRouteImport,
} as any)
+const ChangelogVersionRoute = ChangelogVersionRouteImport.update({
+ id: '/changelog/$version',
+ path: '/changelog/$version',
+ getParentRoute: () => rootRouteImport,
+} as any)
const BlogSlugRoute = BlogSlugRouteImport.update({
id: '/blog/$slug',
path: '/blog/$slug',
@@ -415,8 +427,10 @@ export interface FileRoutesByFullPath {
'/api/shortcuts': typeof ApiShortcutsRoute
'/api/templates': typeof ApiTemplatesRoute
'/blog/$slug': typeof BlogSlugRoute
+ '/changelog/$version': typeof ChangelogVersionRoute
'/legal/$slug': typeof LegalSlugRoute
'/blog/': typeof BlogIndexRoute
+ '/changelog/': typeof ChangelogIndexRoute
'/app/account': typeof ViewAppAccountRoute
'/app/checkout': typeof ViewAppCheckoutRoute
'/app/integration': typeof ViewAppIntegrationRoute
@@ -479,8 +493,10 @@ export interface FileRoutesByTo {
'/api/shortcuts': typeof ApiShortcutsRoute
'/api/templates': typeof ApiTemplatesRoute
'/blog/$slug': typeof BlogSlugRoute
+ '/changelog/$version': typeof ChangelogVersionRoute
'/legal/$slug': typeof LegalSlugRoute
'/blog': typeof BlogIndexRoute
+ '/changelog': typeof ChangelogIndexRoute
'/app/account': typeof ViewAppAccountRoute
'/app/checkout': typeof ViewAppCheckoutRoute
'/app/integration': typeof ViewAppIntegrationRoute
@@ -546,8 +562,10 @@ export interface FileRoutesById {
'/api/shortcuts': typeof ApiShortcutsRoute
'/api/templates': typeof ApiTemplatesRoute
'/blog/$slug': typeof BlogSlugRoute
+ '/changelog/$version': typeof ChangelogVersionRoute
'/legal/$slug': typeof LegalSlugRoute
'/blog/': typeof BlogIndexRoute
+ '/changelog/': typeof ChangelogIndexRoute
'/_view/app/account': typeof ViewAppAccountRoute
'/_view/app/checkout': typeof ViewAppCheckoutRoute
'/_view/app/integration': typeof ViewAppIntegrationRoute
@@ -613,8 +631,10 @@ export interface FileRouteTypes {
| '/api/shortcuts'
| '/api/templates'
| '/blog/$slug'
+ | '/changelog/$version'
| '/legal/$slug'
| '/blog/'
+ | '/changelog/'
| '/app/account'
| '/app/checkout'
| '/app/integration'
@@ -677,8 +697,10 @@ export interface FileRouteTypes {
| '/api/shortcuts'
| '/api/templates'
| '/blog/$slug'
+ | '/changelog/$version'
| '/legal/$slug'
| '/blog'
+ | '/changelog'
| '/app/account'
| '/app/checkout'
| '/app/integration'
@@ -743,8 +765,10 @@ export interface FileRouteTypes {
| '/api/shortcuts'
| '/api/templates'
| '/blog/$slug'
+ | '/changelog/$version'
| '/legal/$slug'
| '/blog/'
+ | '/changelog/'
| '/_view/app/account'
| '/_view/app/checkout'
| '/_view/app/integration'
@@ -808,8 +832,10 @@ export interface RootRouteChildren {
ApiShortcutsRoute: typeof ApiShortcutsRoute
ApiTemplatesRoute: typeof ApiTemplatesRoute
BlogSlugRoute: typeof BlogSlugRoute
+ ChangelogVersionRoute: typeof ChangelogVersionRoute
LegalSlugRoute: typeof LegalSlugRoute
BlogIndexRoute: typeof BlogIndexRoute
+ ChangelogIndexRoute: typeof ChangelogIndexRoute
ApiAssetsSplatRoute: typeof ApiAssetsSplatRoute
ApiTweetIdRoute: typeof ApiTweetIdRoute
ApiWebhooksSlackInteractiveRoute: typeof ApiWebhooksSlackInteractiveRoute
@@ -890,6 +916,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/changelog/': {
+ id: '/changelog/'
+ path: '/changelog'
+ fullPath: '/changelog/'
+ preLoaderRoute: typeof ChangelogIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/blog/': {
id: '/blog/'
path: '/blog'
@@ -904,6 +937,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LegalSlugRouteImport
parentRoute: typeof rootRouteImport
}
+ '/changelog/$version': {
+ id: '/changelog/$version'
+ path: '/changelog/$version'
+ fullPath: '/changelog/$version'
+ preLoaderRoute: typeof ChangelogVersionRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/blog/$slug': {
id: '/blog/$slug'
path: '/blog/$slug'
@@ -1366,8 +1406,10 @@ const rootRouteChildren: RootRouteChildren = {
ApiShortcutsRoute: ApiShortcutsRoute,
ApiTemplatesRoute: ApiTemplatesRoute,
BlogSlugRoute: BlogSlugRoute,
+ ChangelogVersionRoute: ChangelogVersionRoute,
LegalSlugRoute: LegalSlugRoute,
BlogIndexRoute: BlogIndexRoute,
+ ChangelogIndexRoute: ChangelogIndexRoute,
ApiAssetsSplatRoute: ApiAssetsSplatRoute,
ApiTweetIdRoute: ApiTweetIdRoute,
ApiWebhooksSlackInteractiveRoute: ApiWebhooksSlackInteractiveRoute,
diff --git a/apps/web/src/routes/changelog/$version.tsx b/apps/web/src/routes/changelog/$version.tsx
new file mode 100644
index 0000000000..e161f0e053
--- /dev/null
+++ b/apps/web/src/routes/changelog/$version.tsx
@@ -0,0 +1,89 @@
+import { createFileRoute, Link, notFound } from "@tanstack/react-router";
+
+import { ChangelogContent } from "@hypr/changelog";
+
+import { SiteFooter } from "@/components/site-footer";
+import { formatChangelogDate, getChangelogEntry } from "@/lib/changelog";
+import { ANARLOG_SITE_URL } from "@/lib/seo";
+
+export const Route = createFileRoute("/changelog/$version")({
+ component: Component,
+ loader: async ({ params }) => {
+ const entry = getChangelogEntry(params.version);
+ if (!entry) {
+ throw notFound();
+ }
+ return { entry };
+ },
+ head: ({ loaderData }) => {
+ const entry = loaderData?.entry;
+ if (!entry) return {};
+
+ const url = `${ANARLOG_SITE_URL}/changelog/${entry.version}`;
+ return {
+ links: [{ rel: "canonical", href: url }],
+ meta: [
+ { title: `Anarlog v${entry.version} Changelog` },
+ {
+ name: "description",
+ content: `Release notes for Anarlog v${entry.version}.`,
+ },
+ {
+ property: "og:title",
+ content: `Anarlog v${entry.version} Changelog`,
+ },
+ {
+ property: "og:description",
+ content: `Release notes for Anarlog v${entry.version}.`,
+ },
+ { property: "og:url", content: url },
+ ],
+ };
+ },
+});
+
+function Component() {
+ const { entry } = Route.useLoaderData();
+
+ return (
+
+
+
+
+
+
+
+
+
+ ← Changelog
+
+
+
+
+ v{entry.version}
+
+ {entry.date && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/routes/changelog/index.tsx b/apps/web/src/routes/changelog/index.tsx
new file mode 100644
index 0000000000..d2893703cb
--- /dev/null
+++ b/apps/web/src/routes/changelog/index.tsx
@@ -0,0 +1,106 @@
+import { createFileRoute, Link } from "@tanstack/react-router";
+
+import { SiteFooter } from "@/components/site-footer";
+import { changelogEntries, formatChangelogDate } from "@/lib/changelog";
+import { ANARLOG_SITE_URL } from "@/lib/seo";
+
+export const Route = createFileRoute("/changelog/")({
+ component: Component,
+ head: () => ({
+ links: [{ rel: "canonical", href: `${ANARLOG_SITE_URL}/changelog` }],
+ meta: [
+ { title: "Anarlog Changelog" },
+ {
+ name: "description",
+ content:
+ "See the latest Anarlog desktop app updates, fixes, and product changes.",
+ },
+ { property: "og:title", content: "Anarlog Changelog" },
+ { property: "og:url", content: `${ANARLOG_SITE_URL}/changelog` },
+ ],
+ }),
+});
+
+function Component() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ Changelog
+
+
+ Product updates, fixes, and release notes for Anarlog.
+
+
+
+ {changelogEntries.length > 0 ? (
+
+ {changelogEntries.map((entry) => (
+ -
+
+
+
+
+ v{entry.version}
+
+
+ {entry.date && (
+
+ )}
+
+
+ {getEntrySummary(entry.content)}
+
+
+ Read release notes
+
+
+
+ ))}
+
+ ) : (
+
+ No changelog entries yet.
+
+ )}
+
+
+
+
+ );
+}
+
+function getEntrySummary(content: string) {
+ return (
+ content
+ .split("\n")
+ .map((line) => line.trim())
+ .find(
+ (line) => line && !line.startsWith("#") && !line.startsWith("!["),
+ ) ?? "See what changed in this release."
+ );
+}
diff --git a/apps/web/src/utils/sitemap.ts b/apps/web/src/utils/sitemap.ts
index b55f40d3b8..3062cd5899 100644
--- a/apps/web/src/utils/sitemap.ts
+++ b/apps/web/src/utils/sitemap.ts
@@ -18,8 +18,21 @@ function getArticleSlugs(): string[] {
}
}
+function getChangelogVersions(): string[] {
+ const dir = path.resolve(process.cwd(), "../../packages/changelog/content");
+ try {
+ return fs
+ .readdirSync(dir)
+ .filter((f) => f.endsWith(".md"))
+ .map((f) => f.replace(/\.md$/, ""));
+ } catch {
+ return [];
+ }
+}
+
export function getSitemap(): Sitemap {
const slugs = getArticleSlugs();
+ const changelogVersions = getChangelogVersions();
return {
siteUrl: "https://anarlog.so",
@@ -34,6 +47,15 @@ export function getSitemap(): Sitemap {
priority: 0.8,
changeFrequency: "weekly",
},
+ "/changelog/": {
+ priority: 0.7,
+ changeFrequency: "weekly",
+ },
+ "/changelog/$version": changelogVersions.map((version) => ({
+ path: `/changelog/${version}`,
+ priority: 0.5,
+ changeFrequency: "monthly" as const,
+ })),
"/blog/$slug": slugs.map((slug) => ({
path: `/blog/${slug}`,
priority: 0.6,