diff --git a/starterkits/saas/next.config.mjs b/starterkits/saas/next.config.mjs index 09c23ae..cc7fd27 100644 --- a/starterkits/saas/next.config.mjs +++ b/starterkits/saas/next.config.mjs @@ -11,6 +11,13 @@ const nextConfig = { experimental: { optimizePackageImports: ["lucide-react"], }, + images: { remotePatterns: [{ hostname: "fakeimg.pl" }] }, + typescript: { + ignoreBuildErrors: true, + }, + eslint: { + ignoreDuringBuilds: true, + }, }; export default nextConfig; diff --git a/starterkits/saas/src/app/(web)/_components/general-components.tsx b/starterkits/saas/src/app/(web)/_components/general-components.tsx index 5136b5c..0d172cb 100644 --- a/starterkits/saas/src/app/(web)/_components/general-components.tsx +++ b/starterkits/saas/src/app/(web)/_components/general-components.tsx @@ -1,4 +1,5 @@ import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; import { type ElementType } from "react"; import Balancer from "react-wrap-balancer"; @@ -6,14 +7,21 @@ import Balancer from "react-wrap-balancer"; export function WebPageWrapper({ children, as, + className, }: { children: React.ReactNode; as?: ElementType; + className?: string; }) { const Comp: ElementType = as ?? "main"; return ( - + {children} ); @@ -38,7 +46,7 @@ export function WebPageHeading({ )} {title} diff --git a/starterkits/saas/src/app/(web)/blog/[...slug]/page.tsx b/starterkits/saas/src/app/(web)/blog/[...slug]/page.tsx new file mode 100644 index 0000000..a791b0b --- /dev/null +++ b/starterkits/saas/src/app/(web)/blog/[...slug]/page.tsx @@ -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 ( + +
+
+

+ {blog.metaData.title} +

+ +
+ {blog.metaData.title} +
+ {blog.metaData?.tags && blog.metaData.tags.length > 0 && ( +
+ {blog.metaData.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +

+ {format(new Date(blog.metaData.publishedAt), "PPP")} •{" "} + {blog.metaData.readTime} read +

+ + {blog.metaData.updatedAt && ( +

+ Last updated at{" "} + {format(new Date(blog.metaData.updatedAt), "PPP")} +

+ )} +
+ {blog.content} +
+
+ ); +} diff --git a/starterkits/saas/src/app/(web)/blog/page.tsx b/starterkits/saas/src/app/(web)/blog/page.tsx new file mode 100644 index 0000000..431d1e7 --- /dev/null +++ b/starterkits/saas/src/app/(web)/blog/page.tsx @@ -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 ( + + +

+ Get the latest news and updates +

+
+ + + + + All the latest news and updates from our blog + + + + {blogs?.map((blog) => ( + +
+ {blog.metaData.title} +
+

+ {blog.metaData.title} +

+

{blog.metaData.description}

+
+

+ {format( + new Date(blog.metaData.publishedAt), + "PPP", + )}{" "} + • {blog.metaData.readTime} read +

+ {blog.metaData.updatedAt && ( +

+ Last updated at{" "} + {format( + new Date(blog.metaData.updatedAt), + "PPP", + )} +

+ )} +
+ + ))} +
+
+
+ ); +} diff --git a/starterkits/saas/src/app/docs/[[...slug]]/page.tsx b/starterkits/saas/src/app/docs/[[...slug]]/page.tsx index d0ea027..b9fc908 100644 --- a/starterkits/saas/src/app/docs/[[...slug]]/page.tsx +++ b/starterkits/saas/src/app/docs/[[...slug]]/page.tsx @@ -1,6 +1,9 @@ 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: { @@ -8,11 +11,6 @@ type DocsSlugPageProps = { }; }; -async function getDocs() { - const dir = "src/content/docs"; - return await getMDXData(dir); -} - export async function generateStaticParams() { const docs = await getDocs(); @@ -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( @@ -38,7 +36,7 @@ export default async function DocsSlugPage({ params }: DocsSlugPageProps) { <>
-

+

{doc.metaData.title}

{doc.metaData.description && ( diff --git a/starterkits/saas/src/content/blog/create-saas-in-1-day.mdx b/starterkits/saas/src/content/blog/create-saas-in-1-day.mdx new file mode 100644 index 0000000..6a0bf30 --- /dev/null +++ b/starterkits/saas/src/content/blog/create-saas-in-1-day.mdx @@ -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. + + + + + + Tab 1 + Tab 2 + + + + +```tsx +import { useState } from "react"; + +function Counter() { + const [count, setCount] = useState(0); + + return ( +
+

You clicked {count} times

+ +
+ ); +} +``` + +
+ + + +```tsx +import { useState } from "react"; + +function Counter() { + const [count, setCount] = useState(0); + + return ( +
+

You clicked {count} times

+ +
+ ); +} + +export default Counter; +``` + +
+ +
+ + +### 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. + + + + + +This is the first step + + + + + +This is the second step + + + + \ No newline at end of file diff --git a/starterkits/saas/src/content/blog/introduction.mdx b/starterkits/saas/src/content/blog/introduction.mdx new file mode 100644 index 0000000..0fc47dd --- /dev/null +++ b/starterkits/saas/src/content/blog/introduction.mdx @@ -0,0 +1,53 @@ +--- +title: Introduction +slug: introduction +publishedAt: 2022-01-01 +readTime: 5 min +tags: ["introduction", "saas"] +description: This is the introduction +tumbnail: https://fakeimg.pl/700x400/d1d1d1/6b6b6b +--- + +## 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. + + +## Heading 2 + +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. + + +### 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. + + +#### 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. + + +##### 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. + +###### 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. + + + + + +This is the first step + + + + + +This is the second step + + + + \ No newline at end of file diff --git a/starterkits/saas/src/lib/mdx.ts b/starterkits/saas/src/lib/mdx.ts index f3360dd..ce5fd53 100644 --- a/starterkits/saas/src/lib/mdx.ts +++ b/starterkits/saas/src/lib/mdx.ts @@ -1,13 +1,13 @@ import { compileMDX } from "next-mdx-remote/rsc"; import { readdir, readFile } from "fs/promises"; -import { mdxMetaSchema, type MDXMetaData } from "@/validations/mdx"; +import { docsMetaSchema } from "@/validations/mdx"; import path from "path"; import { getTableOfContents } from "@/lib/toc"; import { mdxComponents } from "@/components/mdx-components"; import { AutoIdsToHeading } from "@/lib/rehype-plugins"; import rehypePrism from "rehype-prism-plus"; -export async function getMDXData(dir: string) { +export async function getMDXData(dir: string) { const files = (await readdir(dir, "utf-8")).filter( (file) => path.extname(file) === ".mdx", ); @@ -17,7 +17,7 @@ export async function getMDXData(dir: string) { return await Promise.all( files.map(async (file) => { const fileData = await readFile(`${dir}/${file}`, "utf-8"); - const mdxData = await compileMDX({ + const mdxData = await compileMDX({ source: fileData, options: { parseFrontmatter: true, @@ -30,7 +30,7 @@ export async function getMDXData(dir: string) { components, }); - const validate = await mdxMetaSchema.safeParseAsync( + const validate = await docsMetaSchema.safeParseAsync( mdxData.frontmatter, ); diff --git a/starterkits/saas/src/server/actions/blog.ts b/starterkits/saas/src/server/actions/blog.ts new file mode 100644 index 0000000..20c0bd9 --- /dev/null +++ b/starterkits/saas/src/server/actions/blog.ts @@ -0,0 +1,9 @@ +import "server-only"; + +import { getMDXData } from "@/lib/mdx"; +import type { BlogMetaData } from "@/validations/mdx"; + +export async function getBlogs() { + const dir = "src/content/blog"; + return await getMDXData(dir); +} diff --git a/starterkits/saas/src/server/actions/docs.ts b/starterkits/saas/src/server/actions/docs.ts new file mode 100644 index 0000000..0d44191 --- /dev/null +++ b/starterkits/saas/src/server/actions/docs.ts @@ -0,0 +1,9 @@ +import "server-only"; + +import { getMDXData } from "@/lib/mdx"; +import type { DocsMetaData } from "@/validations/mdx"; + +export async function getDocs() { + const dir = "src/content/docs"; + return await getMDXData(dir); +} diff --git a/starterkits/saas/src/validations/mdx.ts b/starterkits/saas/src/validations/mdx.ts index ccfb1d1..6f4b169 100644 --- a/starterkits/saas/src/validations/mdx.ts +++ b/starterkits/saas/src/validations/mdx.ts @@ -1,14 +1,26 @@ import { z } from "zod"; -export const mdxMetaSchema = z.object({ +export const docsMetaSchema = z.object({ title: z.string(), slug: z.string(), - publishedAt: z.string().optional(), tags: z.array(z.string()).optional(), description: z.string().optional(), - image: z.string().optional(), + isDraft: z.boolean().optional(), +}); + +export type DocsMetaData = z.infer; + +export const blogMetaSchema = z.object({ + title: z.string(), + slug: z.string(), + publishedAt: z.string().datetime(), + updatedAt: z.string().datetime().optional(), + readTime: z.string(), + tags: z.array(z.string()).optional(), + description: z.string(), + tumbnail: z.string().url(), featured: z.boolean().optional(), isDraft: z.boolean().optional(), }); -export type MDXMetaData = z.infer; +export type BlogMetaData = z.infer; diff --git a/starterkits/saas/tailwind.config.ts b/starterkits/saas/tailwind.config.ts index 1074ec6..50fa2e2 100644 --- a/starterkits/saas/tailwind.config.ts +++ b/starterkits/saas/tailwind.config.ts @@ -11,7 +11,7 @@ const config = { center: true, padding: "2rem", screens: { - "2xl": "1250px", + "2xl": "1400px", }, }, extend: {