diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5cb102b --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem +.vscode + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..67be6f0 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Portfolio Blog Starter + +This is a porfolio site template complete with a blog. Includes: + +- MDX and Markdown support +- Optimized for SEO (sitemap, robots, JSON-LD schema) +- RSS Feed +- Dynamic OG images +- Syntax highlighting +- Tailwind v4 +- Vercel Speed Insights / Web Analytics +- Geist font + +## Demo + +https://portfolio-blog-starter.vercel.app + +## How to Use + +You can choose from one of the following two methods to use this repository: + +### One-Click Deploy + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/solutions/blog&project-name=blog&repository-name=blog) + +### Clone and Deploy + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [pnpm](https://pnpm.io/installation) to bootstrap the example: + +```bash +pnpm create next-app --example https://github.com/vercel/examples/tree/main/solutions/blog blog +``` + +Then, run Next.js in development mode: + +```bash +pnpm dev +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/templates) ([Documentation](https://nextjs.org/docs/app/building-your-application/deploying)). diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..7714cd8 --- /dev/null +++ b/app/blog/[slug]/page.tsx @@ -0,0 +1,96 @@ +import { notFound } from 'next/navigation' +import { CustomMDX } from 'app/components/mdx' +import { formatDate, getBlogPosts } from 'app/blog/utils' +import { baseUrl } from 'app/sitemap' + +export async function generateStaticParams() { + let posts = getBlogPosts() + + return posts.map((post) => ({ + slug: post.slug, + })) +} + +export function generateMetadata({ params }) { + let post = getBlogPosts().find((post) => post.slug === params.slug) + if (!post) { + return + } + + let { + title, + publishedAt: publishedTime, + summary: description, + image, + } = post.metadata + let ogImage = image ? image : `${baseUrl}/og?title=${encodeURIComponent(title)}` + + return { + title, + description, + openGraph: { + title, + description, + type: 'article', + publishedTime, + url: `${baseUrl}/blog/${post.slug}`, + images: [ + { + url: ogImage, + }, + ], + }, + twitter: { + card: 'summary_large_image', + title, + description, + images: [ogImage], + }, + } +} + +export default function Blog({ params }) { + let post = getBlogPosts().find((post) => post.slug === params.slug) + + if (!post) { + notFound() + } + + return ( +
+