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 ( +
+
+
+ + Anarlog + +
+ + + ← 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 ( +
+
+
+ + Anarlog + +
+ +
+

+ Changelog +

+

+ Product updates, fixes, and release notes for Anarlog. +

+
+ + {changelogEntries.length > 0 ? ( +
    + {changelogEntries.map((entry) => ( +
  1. +
    +
    + +

    + v{entry.version} +

    + + {entry.date && ( + + )} +
    +

    + {getEntrySummary(entry.content)} +

    + + Read release notes + +
    +
  2. + ))} +
+ ) : ( +

+ 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,