diff --git a/apps/web/content-collections.ts b/apps/web/content-collections.ts index c0b7f8b7fe..b7135bd8dd 100644 --- a/apps/web/content-collections.ts +++ b/apps/web/content-collections.ts @@ -40,7 +40,7 @@ const articles = defineCollection({ display_title: z.string().optional(), meta_title: z.string(), meta_description: z.string(), - author: z.string(), + author: z.enum(["Harshika", "John Jeong", "Yujong Lee"]), created: z.string(), updated: z.string().optional(), coverImage: z.string().optional(), @@ -133,6 +133,7 @@ const docs = defineCollection({ exclude: ["AGENTS.md", "hooks/**", "deeplinks/**"], schema: z.object({ title: z.string(), + section: z.string(), summary: z.string().optional(), category: z.string().optional(), author: z.string().optional(), @@ -163,16 +164,25 @@ const docs = defineCollection({ const sectionFolder = pathParts[0] || "general"; - const slug = document._meta.path.replace(/\.mdx$/, ""); - const isIndex = fileName === "index"; + const orderMatch = fileName.match(/^(\d+)\./); + const order = orderMatch ? parseInt(orderMatch[1], 10) : 999; + + const cleanFileName = fileName.replace(/^\d+\./, ""); + const cleanPath = + pathParts.length > 0 + ? `${pathParts.join("/")}/${cleanFileName}` + : cleanFileName; + const slug = cleanPath; + return { ...document, mdx, slug, sectionFolder, isIndex, + order, toc, }; }, diff --git a/apps/web/content/docs/index.mdx b/apps/web/content/docs/about/0.hello-world.mdx similarity index 98% rename from apps/web/content/docs/index.mdx rename to apps/web/content/docs/about/0.hello-world.mdx index 5bdb4da94e..73c1c38aae 100644 --- a/apps/web/content/docs/index.mdx +++ b/apps/web/content/docs/about/0.hello-world.mdx @@ -1,5 +1,6 @@ --- title: "Hello World!" +section: "About" description: "We are making a world where work is simply talking to others and thinking deeply on your own — nothing else needs to be done." --- diff --git a/apps/web/content/docs/about-hyprnote/what-is-hyprnote.mdx b/apps/web/content/docs/about/1.what-is-hyprnote.mdx similarity index 98% rename from apps/web/content/docs/about-hyprnote/what-is-hyprnote.mdx rename to apps/web/content/docs/about/1.what-is-hyprnote.mdx index 88e161d18f..0e41153f3a 100644 --- a/apps/web/content/docs/about-hyprnote/what-is-hyprnote.mdx +++ b/apps/web/content/docs/about/1.what-is-hyprnote.mdx @@ -1,5 +1,6 @@ --- title: "What is Hyprnote?" +section: "About" description: "Hyprnote is a privacy-first AI notepad for meetings. Think of it as the open-source version of Granola — except that we're cooler." --- diff --git a/apps/web/content/docs/about-hyprnote/why-local-first.mdx b/apps/web/content/docs/about/2.why-local-first.mdx similarity index 98% rename from apps/web/content/docs/about-hyprnote/why-local-first.mdx rename to apps/web/content/docs/about/2.why-local-first.mdx index 0172b7f4bf..427d0b3b68 100644 --- a/apps/web/content/docs/about-hyprnote/why-local-first.mdx +++ b/apps/web/content/docs/about/2.why-local-first.mdx @@ -1,5 +1,6 @@ --- title: "Why Local-First?" +section: "About" description: "What if I said Excel beats Google Sheets? Not because it's Microsoft(lol), but because it's local-first." --- diff --git a/apps/web/content/docs/about-hyprnote/why-self-hosted.mdx b/apps/web/content/docs/about/3.why-self-hosted.mdx similarity index 97% rename from apps/web/content/docs/about-hyprnote/why-self-hosted.mdx rename to apps/web/content/docs/about/3.why-self-hosted.mdx index e6c8175dc8..7dc4642b79 100644 --- a/apps/web/content/docs/about-hyprnote/why-self-hosted.mdx +++ b/apps/web/content/docs/about/3.why-self-hosted.mdx @@ -1,5 +1,6 @@ --- title: "Why Self-Hosted?" +section: "About" description: "Why the heck would you want to self-host Hyprnote? Because you can, and because it gives you control over your data and privacy." --- diff --git a/apps/web/content/docs/cli.mdx b/apps/web/content/docs/cli.mdx deleted file mode 100644 index 886c0920d3..0000000000 --- a/apps/web/content/docs/cli.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: CLI -description: Learn how to use CLI in Hyprnote ---- diff --git a/apps/web/content/docs/analytics.mdx b/apps/web/content/docs/developers/0.analytics.mdx similarity index 70% rename from apps/web/content/docs/analytics.mdx rename to apps/web/content/docs/developers/0.analytics.mdx index ecf0439236..cf317fa53d 100644 --- a/apps/web/content/docs/analytics.mdx +++ b/apps/web/content/docs/developers/0.analytics.mdx @@ -1,6 +1,7 @@ --- -title: Analytics -description: Learn about analytics in Hyprnote +title: "Analytics" +section: "Developers" +description: "Learn about analytics in Hyprnote" --- ## Analytics diff --git a/apps/web/content/docs/bug-report.mdx b/apps/web/content/docs/developers/1.bug-report.mdx similarity index 99% rename from apps/web/content/docs/bug-report.mdx rename to apps/web/content/docs/developers/1.bug-report.mdx index 1291c325f0..c8c0bfaf09 100644 --- a/apps/web/content/docs/bug-report.mdx +++ b/apps/web/content/docs/developers/1.bug-report.mdx @@ -1,5 +1,6 @@ --- title: "Bug Reports & Debugging" +section: "Developers" description: "How to report bugs and find log files for troubleshooting Hyprnote" --- diff --git a/apps/web/content/docs/developers/2.cli.mdx b/apps/web/content/docs/developers/2.cli.mdx new file mode 100644 index 0000000000..5467a0d679 --- /dev/null +++ b/apps/web/content/docs/developers/2.cli.mdx @@ -0,0 +1,5 @@ +--- +title: "CLI" +section: "Developers" +description: "Learn how to use CLI in Hyprnote" +--- diff --git a/apps/web/content/docs/deeplinks.mdx b/apps/web/content/docs/developers/3.deeplinks.mdx similarity index 65% rename from apps/web/content/docs/deeplinks.mdx rename to apps/web/content/docs/developers/3.deeplinks.mdx index dc18de9570..8817a826a3 100644 --- a/apps/web/content/docs/deeplinks.mdx +++ b/apps/web/content/docs/developers/3.deeplinks.mdx @@ -1,6 +1,7 @@ --- -title: Deeplinks -summary: Learn how to use deeplinks in Hyprnote +title: "Deeplinks" +section: "Developers" +summary: "Learn how to use deeplinks in Hyprnote" --- # Overview diff --git a/apps/web/content/docs/hooks.mdx b/apps/web/content/docs/developers/4.hooks.mdx similarity index 85% rename from apps/web/content/docs/hooks.mdx rename to apps/web/content/docs/developers/4.hooks.mdx index 9e616234b5..8e12a1ff0a 100644 --- a/apps/web/content/docs/hooks.mdx +++ b/apps/web/content/docs/developers/4.hooks.mdx @@ -1,6 +1,7 @@ --- -title: Hooks -description: Learn how to use hooks in Hyprnote +title: "Hooks" +section: "Developers" +description: "Learn how to use hooks in Hyprnote" --- # Overview diff --git a/apps/web/content/docs/developers/run.mdx b/apps/web/content/docs/developers/5.run.mdx similarity index 92% rename from apps/web/content/docs/developers/run.mdx rename to apps/web/content/docs/developers/5.run.mdx index 2eaaf4842f..2c4fbd5c9c 100644 --- a/apps/web/content/docs/developers/run.mdx +++ b/apps/web/content/docs/developers/5.run.mdx @@ -1,6 +1,7 @@ --- -title: Run -description: Learn how to run Hyprnote for development +title: "Run" +section: "Developers" +description: "Learn how to run Hyprnote for development" --- # Development diff --git a/apps/web/content/docs/storage.mdx b/apps/web/content/docs/developers/6.storage.mdx similarity index 77% rename from apps/web/content/docs/storage.mdx rename to apps/web/content/docs/developers/6.storage.mdx index f630f2be14..0b925eb68c 100644 --- a/apps/web/content/docs/storage.mdx +++ b/apps/web/content/docs/developers/6.storage.mdx @@ -1,6 +1,7 @@ --- -title: Storage -description: Learn about storage in Hyprnote +title: "Storage" +section: "Developers" +description: "Learn about storage in Hyprnote" --- # How Hyprnote stores data diff --git a/apps/web/content/docs/versioning.mdx b/apps/web/content/docs/developers/7.versioning.mdx similarity index 91% rename from apps/web/content/docs/versioning.mdx rename to apps/web/content/docs/developers/7.versioning.mdx index e20262c59f..f347d55bf7 100644 --- a/apps/web/content/docs/versioning.mdx +++ b/apps/web/content/docs/developers/7.versioning.mdx @@ -1,6 +1,7 @@ --- -title: Versioning -description: How Hyprnote handles versioning for desktop releases +title: "Versioning" +section: "Developers" +description: "How Hyprnote handles versioning for desktop releases" --- # Overview diff --git a/apps/web/content/docs/developers/ai.mdx b/apps/web/content/docs/developers/ai.mdx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/web/content/docs/pro/activation.mdx b/apps/web/content/docs/pro/0.activation.mdx similarity index 98% rename from apps/web/content/docs/pro/activation.mdx rename to apps/web/content/docs/pro/0.activation.mdx index 2b8b840026..e39f88a255 100644 --- a/apps/web/content/docs/pro/activation.mdx +++ b/apps/web/content/docs/pro/0.activation.mdx @@ -1,5 +1,6 @@ --- title: "License Activation" +section: "Pro" description: "Learn how to activate Hyprnote Pro License" --- diff --git a/apps/web/content/docs/pro/better-transcription.mdx b/apps/web/content/docs/pro/1.better-transcription.mdx similarity index 84% rename from apps/web/content/docs/pro/better-transcription.mdx rename to apps/web/content/docs/pro/1.better-transcription.mdx index 6ed7c100ac..778596779b 100644 --- a/apps/web/content/docs/pro/better-transcription.mdx +++ b/apps/web/content/docs/pro/1.better-transcription.mdx @@ -1,4 +1,5 @@ --- title: "Better Transcription" +section: "Pro" description: "Description of your new file." --- diff --git a/apps/web/content/docs/pro/cloud.mdx b/apps/web/content/docs/pro/2.cloud.mdx similarity index 98% rename from apps/web/content/docs/pro/cloud.mdx rename to apps/web/content/docs/pro/2.cloud.mdx index e984c26afa..3209d80533 100644 --- a/apps/web/content/docs/pro/cloud.mdx +++ b/apps/web/content/docs/pro/2.cloud.mdx @@ -1,5 +1,6 @@ --- title: "Cloud" +section: "Pro" description: "Managed cloud service for pro users" --- diff --git a/apps/web/netlify/edge-functions/og.tsx b/apps/web/netlify/edge-functions/og.tsx index b695598a07..6461d65ca7 100644 --- a/apps/web/netlify/edge-functions/og.tsx +++ b/apps/web/netlify/edge-functions/og.tsx @@ -18,11 +18,14 @@ const blogSchema = z.object({ type: z.literal("blog"), title: z.string(), description: z.string().optional(), + author: z.string(), + date: z.string(), }); const docsSchema = z.object({ type: z.literal("docs"), title: z.string(), + section: z.string(), description: z.string().optional(), }); @@ -55,16 +58,19 @@ function parseSearchParams(url: URL): z.infer | null { if (type === "blog") { const title = url.searchParams.get("title"); const description = url.searchParams.get("description") || undefined; + const author = url.searchParams.get("author") || undefined; + const date = url.searchParams.get("date") || undefined; - const result = OGSchema.safeParse({ type, title, description }); + const result = OGSchema.safeParse({ type, title, description, author, date }); return result.success ? result.data : null; } if (type === "docs") { const title = url.searchParams.get("title"); + const section = url.searchParams.get("section"); const description = url.searchParams.get("description") || undefined; - const result = OGSchema.safeParse({ type, title, description }); + const result = OGSchema.safeParse({ type, title, section, description }); return result.success ? result.data : null; } @@ -142,57 +148,76 @@ function renderChangelogTemplate(params: z.infer) { if (isNightly) { return ( -
-
Changelog
-
v.{params.version}
-
The AI notepad for private meetings
-
Hyprnote.
-
- +
+
Changelog
+
v.{params.version}
+
The AI notepad for private meetings
+
Hyprnote.
+
+
); } return ( -
-
Changelog
-
v.{params.version}
-
The AI notepad for private meetings
-
Hyprnote.
-
- +
+
Changelog
+
v.{params.version}
+
The AI notepad for private meetings
+
Hyprnote.
+
+
); } +function getAuthorAvatar(author: string): string { + const authorMap: Record = { + "John Jeong": "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/john.png", + "Yujong Lee": "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/yujong.png", + }; + + return authorMap[author] || "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/icons/stable-icon.png"; +} + function renderBlogTemplate(params: z.infer) { + const avatarUrl = getAuthorAvatar(params.author); + return ( -
-
{preventWidow(params.title)}
- {params.description && ( -
{params.description}
- )} -
The AI notepad for private meetings
-
Hyprnote.
-
-
Blog
- +
+
+
{preventWidow(params.title)}
+
+ +
{params.author}
+
+
{params.date}
+
+
+
The AI notepad for private meetings
+
+ +
Hyprnote.
+
+
); } function renderDocsTemplate(params: z.infer) { return ( -
-
{preventWidow(params.title)}
+
+
+ +
Hyprnote Docs
+
+
+
{params.section}
+
{preventWidow(params.title)}
{params.description && ( -
{params.description}
+
{params.description}
)} -
The AI notepad for private meetings
-
Hyprnote.
-
-
Documentation
- +
); } @@ -236,6 +261,14 @@ export default async function handler(req: Request) { weight: 700 as const, style: "normal" as const, }, + { + name: "Lora", + data: await fetch( + "https://fonts.gstatic.com/s/lora/v37/0QI6MX1D_JOuGQbT0gvTJPa787weuyJGmKxemMeZ.ttf" + ).then((res) => res.arrayBuffer()), + weight: 400 as const, + style: "normal" as const, + }, { name: "IBM Plex Mono", data: await fetch( diff --git a/apps/web/src/routes/_view/blog/$slug.tsx b/apps/web/src/routes/_view/blog/$slug.tsx index 87b7024b26..beb84a25a7 100644 --- a/apps/web/src/routes/_view/blog/$slug.tsx +++ b/apps/web/src/routes/_view/blog/$slug.tsx @@ -36,6 +36,10 @@ export const Route = createFileRoute("/_view/blog/$slug")({ const { article } = loaderData!; const url = `https://hyprnote.com/blog/${article.slug}`; + const ogImage = + article.coverImage || + `https://hyprnote.com/og?type=blog&title=${encodeURIComponent(article.title)}${article.author ? `&author=${encodeURIComponent(article.author)}` : ""}${article.created ? `&date=${encodeURIComponent(new Date(article.created).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }))}` : ""}`; + return { meta: [ { title: article.title }, @@ -44,15 +48,11 @@ export const Route = createFileRoute("/_view/blog/$slug")({ { property: "og:description", content: article.meta_description }, { property: "og:type", content: "article" }, { property: "og:url", content: url }, - ...(article.coverImage - ? [{ property: "og:image", content: article.coverImage }] - : []), + { property: "og:image", content: ogImage }, { name: "twitter:card", content: "summary_large_image" }, { name: "twitter:title", content: article.title }, { name: "twitter:description", content: article.meta_description }, - ...(article.coverImage - ? [{ name: "twitter:image", content: article.coverImage }] - : []), + { name: "twitter:image", content: ogImage }, ...(article.author ? [{ name: "author", content: article.author }] : []), diff --git a/apps/web/src/routes/_view/docs/$.tsx b/apps/web/src/routes/_view/docs/$.tsx index 0a6e62ec1d..f678a9569b 100644 --- a/apps/web/src/routes/_view/docs/$.tsx +++ b/apps/web/src/routes/_view/docs/$.tsx @@ -1,4 +1,4 @@ -import { createFileRoute, notFound } from "@tanstack/react-router"; +import { createFileRoute, notFound, redirect } from "@tanstack/react-router"; import { allDocs } from "content-collections"; import { DocLayout } from "./-components"; @@ -6,8 +6,29 @@ import { DocLayout } from "./-components"; export const Route = createFileRoute("/_view/docs/$")({ component: Component, loader: async ({ params }) => { - const doc = allDocs.find((doc) => doc.slug === params._splat); + const splat = params._splat || ""; + let doc = allDocs.find((doc) => doc.slug === splat); + + if (!doc) { + doc = allDocs.find((doc) => doc.slug === `${splat}/index`); + } + if (!doc) { + const pathParts = splat.split("/"); + const firstPart = pathParts[0]; + const sectionName = + firstPart.charAt(0).toUpperCase() + firstPart.slice(1); + const docsInSection = allDocs + .filter((d) => d.section === sectionName && !d.isIndex) + .sort((a, b) => a.order - b.order); + + if (docsInSection.length > 0) { + throw redirect({ + to: "/docs/$", + params: { _splat: docsInSection[0].slug }, + }); + } + throw notFound(); } @@ -16,6 +37,7 @@ export const Route = createFileRoute("/_view/docs/$")({ head: ({ loaderData }) => { const { doc } = loaderData!; const url = `https://hyprnote.com/docs/${doc.slug}`; + const ogImageUrl = `https://hyprnote.com/og?type=docs&title=${encodeURIComponent(doc.title)}§ion=${encodeURIComponent(doc.section)}${doc.summary ? `&description=${encodeURIComponent(doc.summary)}` : ""}`; return { meta: [ @@ -25,9 +47,11 @@ export const Route = createFileRoute("/_view/docs/$")({ { property: "og:description", content: doc.summary || doc.title }, { property: "og:type", content: "article" }, { property: "og:url", content: url }, - { name: "twitter:card", content: "summary" }, + { property: "og:image", content: ogImageUrl }, + { name: "twitter:card", content: "summary_large_image" }, { name: "twitter:title", content: doc.title }, { name: "twitter:description", content: doc.summary || doc.title }, + { name: "twitter:image", content: ogImageUrl }, ], }; }, diff --git a/apps/web/src/routes/_view/docs/-structure.tsx b/apps/web/src/routes/_view/docs/-structure.tsx new file mode 100644 index 0000000000..c83e3a1f24 --- /dev/null +++ b/apps/web/src/routes/_view/docs/-structure.tsx @@ -0,0 +1,3 @@ +export const docsStructure = { + sections: ["about", "developers", "pro"], +}; diff --git a/apps/web/src/routes/_view/docs/index.tsx b/apps/web/src/routes/_view/docs/index.tsx index 5727bd77a7..b13dec37aa 100644 --- a/apps/web/src/routes/_view/docs/index.tsx +++ b/apps/web/src/routes/_view/docs/index.tsx @@ -1,40 +1,10 @@ -import { createFileRoute, notFound } from "@tanstack/react-router"; -import { allDocs } from "content-collections"; - -import { DocLayout } from "./-components"; +import { createFileRoute, redirect } from "@tanstack/react-router"; export const Route = createFileRoute("/_view/docs/")({ - component: Component, - loader: async () => { - const doc = allDocs.find((doc) => doc.slug === "index"); - if (!doc) { - throw notFound(); - } - - return { doc }; - }, - head: ({ loaderData }) => { - const { doc } = loaderData!; - const url = "https://hyprnote.com/docs"; - - return { - meta: [ - { title: doc.title }, - { name: "description", content: doc.summary || doc.title }, - { property: "og:title", content: doc.title }, - { property: "og:description", content: doc.summary || doc.title }, - { property: "og:type", content: "article" }, - { property: "og:url", content: url }, - { name: "twitter:card", content: "summary" }, - { name: "twitter:title", content: doc.title }, - { name: "twitter:description", content: doc.summary || doc.title }, - ], - }; + beforeLoad: () => { + throw redirect({ + to: "/docs/$", + params: { _splat: "about/hello-world" }, + }); }, }); - -function Component() { - const { doc } = Route.useLoaderData(); - - return ; -} diff --git a/apps/web/src/routes/_view/docs/route.tsx b/apps/web/src/routes/_view/docs/route.tsx index 38dd955c45..9f559d6836 100644 --- a/apps/web/src/routes/_view/docs/route.tsx +++ b/apps/web/src/routes/_view/docs/route.tsx @@ -7,6 +7,8 @@ import { import { allDocs } from "content-collections"; import { useMemo } from "react"; +import { docsStructure } from "./-structure"; + export const Route = createFileRoute("/_view/docs")({ component: Component, }); @@ -33,67 +35,51 @@ function LeftSidebar() { ) as string | undefined; const docsBySection = useMemo(() => { - const grouped = allDocs.reduce( - (acc, doc) => { - if (!acc[doc.sectionFolder]) { - acc[doc.sectionFolder] = { - title: "", - docs: [], - indexDoc: null as typeof doc | null, - }; - } + const sectionGroups: Record< + string, + { title: string; docs: (typeof allDocs)[0][] } + > = {}; - if (doc.isIndex) { - acc[doc.sectionFolder].indexDoc = doc; - acc[doc.sectionFolder].title = doc.title; - } else { - acc[doc.sectionFolder].docs.push(doc); - } + allDocs.forEach((doc) => { + if (doc.slug === "index" || doc.isIndex) { + return; + } - return acc; - }, - {} as Record< - string, - { - title: string; - docs: typeof allDocs; - indexDoc: (typeof allDocs)[0] | null; - } - >, - ); + const sectionName = doc.section; - Object.keys(grouped).forEach((folder) => { - if (!grouped[folder].title) { - grouped[folder].title = folder - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); + if (!sectionGroups[sectionName]) { + sectionGroups[sectionName] = { + title: sectionName, + docs: [], + }; } + + sectionGroups[sectionName].docs.push(doc); + }); + + Object.keys(sectionGroups).forEach((sectionName) => { + sectionGroups[sectionName].docs.sort((a, b) => a.order - b.order); }); - return Object.values(grouped).sort((a, b) => - a.title.localeCompare(b.title), - ); + const sections = docsStructure.sections + .map((sectionId) => { + const sectionName = + sectionId.charAt(0).toUpperCase() + sectionId.slice(1); + return sectionGroups[sectionName]; + }) + .filter(Boolean); + + return { sections }; }, []); return (