Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add help articles #386

Merged
merged 9 commits into from
Apr 24, 2024
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
128 changes: 128 additions & 0 deletions app/(static)/help/article/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { getHelpArticles, getHelpArticle } from "@/lib/content/help";
import { ContentBody } from "@/components/mdx/post-body";
import { notFound } from "next/navigation";
import Link from "next/link";
import BlurImage from "@/components/blur-image";
import { constructMetadata, formatDate } from "@/lib/utils";
import { Metadata } from "next";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import TableOfContents from "@/components/mdx/table-of-contents";

export async function generateStaticParams() {
const articles = await getHelpArticles();
return articles.map((article) => ({ slug: article?.data.slug }));
}

export const generateMetadata = async ({
params,
}: {
params: {
slug: string;
};
}): Promise<Metadata> => {
const article = (await getHelpArticles()).find(
(article) => article?.data.slug === params.slug,
);
const { title, summary: description, image } = article?.data || {};

return constructMetadata({
title: `${title} - Papermark`,
description,
image,
});
};

export default async function BlogPage({
params,
}: {
params: { slug: string };
}) {
const article = await getHelpArticle(params.slug);
if (!article) return notFound();

// const category = article.data.categories ? article.data.categories[0] : "";

return (
<>
<div className="max-w-7xl w-full mx-auto px-4 md:px-8 mb-10">
<div className="flex max-w-screen-sm flex-col space-y-4 pt-16">
<div className="flex items-center space-x-4">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage>Help Center</BreadcrumbPage>
</BreadcrumbItem>
{/* <BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href={`/help/category/${category}`}>{category}</Link>
</BreadcrumbLink>
</BreadcrumbItem> */}
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{article.data.title}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<h1 className="text-4xl md:text-5xl text-balance">
{article.data.title}
</h1>
<p className="text-lg text-gray-600">{article.data.summary}</p>

<div className="items-center space-x-4 flex flex-col self-start">
<Link
href={`https://twitter.com/mfts0`}
className="group flex items-center space-x-3"
target="_blank"
rel="noopener noreferrer"
>
<BlurImage
src={`https://pbs.twimg.com/profile_images/1176854646343852032/iYnUXJ-m_400x400.jpg`}
alt={`Marc Seitz`}
width={40}
height={40}
className="rounded-full transition-all group-hover:brightness-90"
/>
<div className="flex flex-col">
<p className="font-semibold text-gray-700">Marc Seitz</p>
<p className="text-sm text-gray-500">@mfts0</p>
</div>
</Link>
</div>
<div className="flex items-center space-x-4 text-gray-700">
<time dateTime={article.data.publishedAt} className="text-sm">
Last updated {formatDate(article.data.publishedAt, true)}
</time>
</div>
</div>
</div>

<div className="relative">
<div className="grid grid-cols-4 gap-10 py-10 max-w-7xl w-full mx-auto px-4 md:px-8">
<div className="relative col-span-4 mb-10 flex flex-col space-y-8 bg-white md:col-span-3 sm:border-r sm:border-orange-500">
<div
data-mdx-container
className="prose prose-h2:mb-2 first:prose-h2:mt-0 prose-h2:mt-10 prose-headings:font-medium sm:max-w-screen-md sm:pr-2 md:pr-0"
>
<ContentBody>{article.body}</ContentBody>
</div>
</div>

<div className="sticky top-14 col-span-1 hidden flex-col divide-y divide-gray-200 self-start sm:flex">
<div className="flex flex-col space-y-4">
<TableOfContents items={article.toc} />
</div>
</div>
</div>
</div>
</>
);
}
6 changes: 3 additions & 3 deletions app/(static)/solutions/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default async function PagePage({
</p>
<div className="pt-8 space-x-2">
<Link href="/login">
<Button className="text-white text-balance bg-gray-900 rounded-3xl hover:bg-gray-800 justify-center text-balance">
<Button className="text-white text-balance bg-gray-900 rounded-3xl hover:bg-gray-800 justify-center">
{page.button}
</Button>
</Link>
Expand Down Expand Up @@ -99,7 +99,7 @@ export default async function PagePage({
</div>
<div className="pt-8 space-x-2">
<Link href="/login">
<Button className="text-white text-balance bg-gray-900 rounded-3xl hover:bg-gray-800 justify-center text-balance">
<Button className="text-white text-balance bg-gray-900 rounded-3xl hover:bg-gray-800 justify-center">
{page.button}
</Button>
</Link>
Expand Down Expand Up @@ -171,7 +171,7 @@ export default async function PagePage({
</div>
<div className="pt-8 space-x-2">
<Link href="/login">
<Button className="text-white text-balance bg-gray-900 rounded-3xl hover:bg-gray-800 justify-center text-balance">
<Button className="text-white text-balance bg-gray-900 rounded-3xl hover:bg-gray-800 justify-center">
{page.button}
</Button>
</Link>
Expand Down
13 changes: 12 additions & 1 deletion app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { getPosts, getAlternatives, getPages } from "@/lib/content";
import {
getPosts,
getAlternatives,
getPages,
getHelpArticles,
} from "@/lib/content";
import { MetadataRoute } from "next";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts();
const solutions = await getPages();
const alternatives = await getAlternatives();
const helpArticles = await getHelpArticles();
const blogLinks = posts.map((post) => ({
url: `https://www.papermark.io/blog/${post?.data.slug}`,
lastModified: new Date().toISOString().split("T")[0],
Expand All @@ -17,6 +23,10 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
url: `https://www.papermark.io/alternatives/${alternative?.slug}`,
lastModified: new Date().toISOString().split("T")[0],
}));
const helpArticleLinks = helpArticles.map((article) => ({
url: `https://www.papermark.io/help/article/${article?.data.slug}`,
lastModified: new Date().toISOString().split("T")[0],
}));

return [
{
Expand Down Expand Up @@ -54,5 +64,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
...blogLinks,
...solutionLinks,
...alternativeLinks,
...helpArticleLinks,
];
}
18 changes: 18 additions & 0 deletions components/mdx/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,23 @@ export const mdxComponents: MDXComponents = {
</Link>
);
},
h2: ({ children, ...props }) => (
<h2
data-mdx-heading
className="text-2xl font-semibold text-black"
id={props.id}
>
{children}
</h2>
),
h3: ({ children, ...props }) => (
<h3
data-mdx-heading
className="text-xl font-semibold text-black"
id={props.id}
>
{children}
</h3>
),
// any other components you want to use in your markdown
};
105 changes: 105 additions & 0 deletions components/mdx/table-of-contents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"use client";

// import useCurrentAnchor from "#/lib/hooks/use-current-anchor";
import { cn } from "@/lib/utils";
import slugify from "@sindresorhus/slugify";
import Link from "next/link";

export default function TableOfContents({
items,
}: {
items: {
text: string;
level: number;
}[];
}) {
const currentAnchor = useCurrentAnchor();

return (
<div className="grid gap-4 -ml-[2.55rem] pl-10 border-l border-orange-500">
{items &&
items.map((item, idx) => {
const itemId = slugify(item.text);
return (
<Link
key={itemId}
href={`#${itemId}`}
className={cn("text-sm text-gray-500 ", {
"border-l-2 border-black -ml-[2.6rem] pl-10 text-black":
currentAnchor ? currentAnchor === itemId : idx === 0,
})}
>
{item.text}
</Link>
);
})}
</div>
);
}

import { useEffect, useState } from "react";

function useCurrentAnchor() {
const [currentAnchor, setCurrentAnchor] = useState<string | null>(null);

useEffect(() => {
const mdxContainer: HTMLElement | null = document.querySelector(
"[data-mdx-container]",
);

if (!mdxContainer) return;

const offsetTop = 200;

const observer = new IntersectionObserver(
(entries) => {
let currentEntry = entries[0];
if (!currentEntry) return;

const offsetBottom =
(currentEntry.rootBounds?.height || 0) * 0.3 + offsetTop;

for (let i = 1; i < entries.length; i++) {
const entry = entries[i];
if (!entry) break;

if (
entry.boundingClientRect.top <
currentEntry.boundingClientRect.top ||
currentEntry.boundingClientRect.bottom < offsetTop
) {
currentEntry = entry;
}
}

let target: Element | undefined = currentEntry.target;

// if the target is too high up, we need to find the next sibling
while (target && target.getBoundingClientRect().bottom < offsetTop) {
target = siblings.get(target)?.next;
}

// if the target is too low, we need to find the previous sibling
while (target && target.getBoundingClientRect().top > offsetBottom) {
target = siblings.get(target)?.prev;
}
if (target) setCurrentAnchor(target.id);
},
{
threshold: 1,
rootMargin: `-${offsetTop}px 0px 0px 0px`,
},
);

const siblings = new Map();

const anchors = mdxContainer?.querySelectorAll("[data-mdx-heading]");
anchors.forEach((anchor) => observer.observe(anchor));

return () => {
observer.disconnect();
};
}, []);

return currentAnchor?.replace("#", "");
}
4 changes: 4 additions & 0 deletions lib/content/alternative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ type Alternative = {
// this means getPosts() will only be called once per page build, even though we may call it multiple times
// when rendering the page.
export const getAlternatives = async () => {
if (!process.env.CONTENT_BASE_URL) {
return [];
}

const response = await fetch(
`${process.env.CONTENT_BASE_URL}/api/alternatives`,
{
Expand Down
4 changes: 4 additions & 0 deletions lib/content/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const GITHUB_CONTENT_TOKEN = process.env.GITHUB_CONTENT_TOKEN;
const GITHUB_CONTENT_REPO = process.env.GITHUB_CONTENT_REPO;

export const getPostsRemote = cache(async () => {
if (!GITHUB_CONTENT_REPO || !GITHUB_CONTENT_TOKEN) {
return [];
}

const apiUrl = `https://api.github.com/repos/${GITHUB_CONTENT_REPO}/contents/content/blog`;
const headers = {
Authorization: `Bearer ${GITHUB_CONTENT_TOKEN}`,
Expand Down
Loading