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(saas): Blog feature using next-mdx-remote #14

Merged
merged 9 commits into from
Apr 20, 2024
7 changes: 7 additions & 0 deletions starterkits/saas/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ const nextConfig = {
experimental: {
optimizePackageImports: ["lucide-react"],
},
images: { remotePatterns: [{ hostname: "fakeimg.pl" }] },
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
};

export default nextConfig;
12 changes: 10 additions & 2 deletions starterkits/saas/src/app/(web)/_components/general-components.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { type ElementType } from "react";
import Balancer from "react-wrap-balancer";

// This is a page wrapper used in all public web pages
export function WebPageWrapper({
children,
as,
className,
}: {
children: React.ReactNode;
as?: ElementType;
className?: string;
}) {
const Comp: ElementType = as ?? "main";

return (
<Comp className="container flex flex-col items-center justify-center gap-24 py-10">
<Comp
className={cn(
"container flex flex-col items-center justify-center gap-24 py-10",
className,
)}
>
{children}
</Comp>
);
Expand All @@ -38,7 +46,7 @@ export function WebPageHeading({
)}
<Balancer
as="h1"
className="font-heading max-w-2xl text-center text-5xl font-bold leading-none sm:text-6xl"
className="max-w-2xl text-center font-heading text-5xl font-bold leading-none sm:text-6xl"
>
{title}
</Balancer>
Expand Down
79 changes: 79 additions & 0 deletions starterkits/saas/src/app/(web)/blog/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { WebPageWrapper } from "@/app/(web)/_components/general-components";
import { Badge } from "@/components/ui/badge";
import { siteUrls } from "@/config/urls";
import { getBlogs } from "@/server/actions/blog";
import { format } from "date-fns";
import Image from "next/image";
import { notFound, redirect } from "next/navigation";

export const dynamic = "force-static";

type BlogSlugPageProps = {
params: {
slug: string[];
};
};

export async function generateStaticParams() {
const blogs = await getBlogs();

return blogs.map((blog) => ({
slug: blog.metaData.slug.split("/"),
}));
}

export default async function BlogSlugPage({ params }: BlogSlugPageProps) {
if (!params.slug) {
return redirect(siteUrls.blog);
}

const slug = params.slug.join("/");

const blog = (await getBlogs()).find((b) => b.metaData.slug === slug);

if (!blog) {
return notFound();
}

return (
<WebPageWrapper className="relative max-w-3xl flex-row items-start gap-8">
<article className="w-full space-y-10">
<div className="space-y-4">
<h1 className="scroll-m-20 font-heading text-4xl font-bold">
{blog.metaData.title}
</h1>

<div className="relative aspect-video max-h-[350px] w-full overflow-hidden rounded-md bg-muted/60">
<Image
src={blog.metaData.tumbnail}
alt={blog.metaData.title}
className="rounded-md"
fill
/>
</div>
{blog.metaData?.tags && blog.metaData.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{blog.metaData.tags.map((tag) => (
<Badge variant="outline" key={tag}>
{tag}
</Badge>
))}
</div>
)}
<p className="text-sm text-muted-foreground">
{format(new Date(blog.metaData.publishedAt), "PPP")} •{" "}
{blog.metaData.readTime} read
</p>

{blog.metaData.updatedAt && (
<p className="text-sm text-muted-foreground">
Last updated at{" "}
{format(new Date(blog.metaData.updatedAt), "PPP")}
</p>
)}
</div>
{blog.content}
</article>
</WebPageWrapper>
);
}
74 changes: 74 additions & 0 deletions starterkits/saas/src/app/(web)/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
WebPageHeading,
WebPageWrapper,
} from "@/app/(web)/_components/general-components";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { siteUrls } from "@/config/urls";
import { getBlogs } from "@/server/actions/blog";
import { format } from "date-fns";
import Image from "next/image";
import Link from "next/link";

export const dynamic = "force-static";

export default async function BlogsPage() {
const blogs = await getBlogs();

return (
<WebPageWrapper>
<WebPageHeading title="Blog">
<p className="text-center text-base">
<span>Get the latest news and updates</span>
</p>
</WebPageHeading>

<Card className="w-full">
<CardHeader>
<CardTitle>
All the latest news and updates from our blog
</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-2 gap-8">
{blogs?.map((blog) => (
<Link
href={`${siteUrls.blog}/${blog.metaData.slug}`}
key={blog.metaData.slug}
className="space-y-4"
>
<div className="relative h-screen max-h-[350px] w-full overflow-hidden rounded-md bg-muted/60">
<Image
src={blog.metaData.tumbnail}
alt={blog.metaData.title}
fill
className="object-cover"
/>
</div>
<h2 className="font-heading text-2xl font-semibold">
{blog.metaData.title}
</h2>
<p>{blog.metaData.description}</p>
<div className="grid gap-0.5 font-light">
<p className="text-sm text-muted-foreground">
{format(
new Date(blog.metaData.publishedAt),
"PPP",
)}{" "}
• {blog.metaData.readTime} read
</p>
{blog.metaData.updatedAt && (
<p className="text-sm text-muted-foreground">
Last updated at{" "}
{format(
new Date(blog.metaData.updatedAt),
"PPP",
)}
</p>
)}
</div>
</Link>
))}
</CardContent>
</Card>
</WebPageWrapper>
);
}
14 changes: 6 additions & 8 deletions starterkits/saas/src/app/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { notFound, redirect } from "next/navigation";
import { Toc } from "@/components/toc";
import { getMDXData } from "@/lib/mdx";
import { getDocs } from "@/server/actions/docs";
import { siteUrls } from "@/config/urls";

export const dynamic = "force-static";

type DocsSlugPageProps = {
params: {
slug: string[];
};
};

async function getDocs() {
const dir = "src/content/docs";
return await getMDXData(dir);
}

export async function generateStaticParams() {
const docs = await getDocs();

Expand All @@ -23,7 +21,7 @@ export async function generateStaticParams() {

export default async function DocsSlugPage({ params }: DocsSlugPageProps) {
if (!params.slug) {
return redirect("/docs/introduction");
return redirect(siteUrls.docs);
}

const doc = (await getDocs()).find(
Expand All @@ -38,7 +36,7 @@ export default async function DocsSlugPage({ params }: DocsSlugPageProps) {
<>
<article className="flex-1 py-10">
<div className="space-y-2">
<h1 className="font-heading scroll-m-20 text-4xl font-bold">
<h1 className="scroll-m-20 font-heading text-4xl font-bold">
{doc.metaData.title}
</h1>
{doc.metaData.description && (
Expand Down
104 changes: 104 additions & 0 deletions starterkits/saas/src/content/blog/create-saas-in-1-day.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
title: Create a SaaS in 1 day
slug: create-saas-in-1-day
publishedAt: 2022-01-01
updatedAt: 2024-05-01
readTime: 5 min
tags: ["saas", "introduction"]
description: This is the introduction
tumbnail: https://fakeimg.pl/700x400/d1d1d1/6b6b6b
---


## This is the introduction

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.


<Tabs defaultValue='tab-1'>

<TabsList>
<TabsTrigger value="tab-1">Tab 1</TabsTrigger>
<TabsTrigger value="tab-2">Tab 2</TabsTrigger>
</TabsList>

<TabsContent value="tab-1">

```tsx
import { useState } from "react";

function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
```

</TabsContent>

<TabsContent value="tab-2">

```tsx
import { useState } from "react";

function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

export default Counter;
```

</TabsContent>

</Tabs>


### This is the long heading 3

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.


### This is the long heading 4

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.


### This is the long heading 5

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.

### short heading 6

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.

<Steps>

<Step>

This is the first step

</Step>

<Step>

This is the second step

</Step>

</Steps>
Loading
Loading