├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 20240712023225_init │ │ └── migration.sql │ ├── 20240712034116_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── next.svg └── vercel.svg ├── src ├── app │ ├── about │ │ └── page.tsx │ ├── api │ │ └── route.ts │ ├── blog │ │ ├── [category] │ │ │ ├── [slug] │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── contents │ │ │ ├── learn-react.mdx │ │ │ ├── react-19.mdx │ │ │ ├── spaces-vs-tabs.mdx │ │ │ ├── static-typing.mdx │ │ │ └── vim.mdx │ │ ├── layout.tsx │ │ └── utils.ts │ ├── layout.tsx │ ├── not-found.tsx │ ├── og │ │ └── route.tsx │ ├── page.tsx │ ├── privacy-policy │ │ ├── page.tsx │ │ └── privacy-policy.mdx │ ├── robots.ts │ ├── rss │ │ └── route.ts │ ├── sitemap.ts │ └── terms-of-services │ │ ├── page.tsx │ │ └── terms-of-services.mdx ├── components │ ├── Breadcrumb.tsx │ ├── CardCategory.tsx │ ├── Container.tsx │ ├── Header.tsx │ ├── ReportViews.tsx │ ├── SubmitButton.tsx │ ├── footer.tsx │ ├── home │ │ ├── latest-posts.tsx │ │ ├── popular-posts.tsx │ │ └── top-categories.tsx │ ├── icons.tsx │ ├── main-nav.tsx │ ├── mdx.tsx │ ├── skeleton │ │ └── popular_posts_skeleton.tsx │ ├── theme-provider.tsx │ └── ui │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── mode-toggle.tsx │ │ ├── navigation-menu.tsx │ │ └── skeleton.tsx ├── config │ └── site.ts ├── db │ └── index.ts ├── lib │ ├── actions.ts │ ├── constants.ts │ ├── placeholder-data.ts │ └── utils.ts └── styles │ └── globals.css ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Building Next.js Fullstack Blog with TypeScript, Shadcn/ui, MDX, Prisma and Vercel Postgres.](https://img.youtube.com/vi/htgktwXYw6g/0.jpg)](https://www.youtube.com/watch?v=htgktwXYw6g) 3 | 4 | 5 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 6 | 7 | ## Getting Started 8 | 9 | First, run the development server: 10 | 11 | ```bash 12 | npm run dev 13 | # or 14 | yarn dev 15 | # or 16 | pnpm dev 17 | # or 18 | bun dev 19 | ``` 20 | 21 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 22 | 23 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 24 | 25 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 41 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-blog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "postinstall": "prisma generate", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@prisma/client": "^5.16.1", 14 | "@radix-ui/react-dropdown-menu": "^2.1.1", 15 | "@radix-ui/react-navigation-menu": "^1.2.0", 16 | "@radix-ui/react-slot": "^1.1.0", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.1.1", 19 | "gray-matter": "^4.0.3", 20 | "lucide-react": "^0.399.0", 21 | "next": "^14.2.18", 22 | "next-mdx-remote": "^5.0.0", 23 | "next-themes": "^0.3.0", 24 | "react": "^18", 25 | "react-dom": "^18", 26 | "sugar-high": "^0.7.0", 27 | "swr": "^2.2.5", 28 | "tailwind-merge": "^2.3.0", 29 | "tailwindcss-animate": "^1.0.7", 30 | "zod": "^3.23.8" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^20", 34 | "@types/react": "^18", 35 | "@types/react-dom": "^18", 36 | "eslint": "^8", 37 | "eslint-config-next": "14.2.4", 38 | "postcss": "^8", 39 | "prisma": "^5.16.1", 40 | "tailwindcss": "^3.4.1", 41 | "typescript": "^5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240712023225_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Blog" ( 3 | "id" SERIAL NOT NULL, 4 | "slug" TEXT NOT NULL, 5 | "title" TEXT NOT NULL, 6 | "category" TEXT NOT NULL, 7 | "view_count" INTEGER NOT NULL DEFAULT 1, 8 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | 10 | CONSTRAINT "Blog_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateTable 14 | CREATE TABLE "Subscriber" ( 15 | "id" SERIAL NOT NULL, 16 | "email" TEXT NOT NULL, 17 | "is_subscribed" BOOLEAN NOT NULL DEFAULT true, 18 | 19 | CONSTRAINT "Subscriber_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateIndex 23 | CREATE UNIQUE INDEX "Blog_slug_key" ON "Blog"("slug"); 24 | -------------------------------------------------------------------------------- /prisma/migrations/20240712034116_init/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[email]` on the table `Subscriber` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "Subscriber_email_key" ON "Subscriber"("email"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("POSTGRES_PRISMA_URL") // uses connection pooling 14 | directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection 15 | } 16 | 17 | model Blog { 18 | id Int @id @default(autoincrement()) 19 | slug String @unique 20 | title String 21 | category String 22 | view_count Int @default(1) 23 | updatedAt DateTime @default(now()) 24 | } 25 | 26 | model Subscriber { 27 | id Int @id @default(autoincrement()) 28 | email String @unique 29 | is_subscribed Boolean @default(true) 30 | } -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tsadev/next-blog/e6c6c8e0e9c45734f16fea8e120c8ea0b5d500d3/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tsadev/next-blog/e6c6c8e0e9c45734f16fea8e120c8ea0b5d500d3/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tsadev/next-blog/e6c6c8e0e9c45734f16fea8e120c8ea0b5d500d3/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tsadev/next-blog/e6c6c8e0e9c45734f16fea8e120c8ea0b5d500d3/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tsadev/next-blog/e6c6c8e0e9c45734f16fea8e120c8ea0b5d500d3/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tsadev/next-blog/e6c6c8e0e9c45734f16fea8e120c8ea0b5d500d3/public/favicon.ico -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/about/page.tsx: -------------------------------------------------------------------------------- 1 | import Container from "@/components/Container"; 2 | import Header from "@/components/Header"; 3 | import { MainNav } from "@/components/main-nav"; 4 | import { Metadata } from "next"; 5 | 6 | export const metadata: Metadata = { 7 | title: "About Me", 8 | description: "Information about me", 9 | }; 10 | 11 | export default async function AboutPage() { 12 | return ( 13 | <> 14 |
15 | 16 | 17 |
18 |

19 | About Me 20 |

21 |
22 |
23 |
24 |
25 |
26 |
27 |

28 | Software Developer 29 |

30 |
31 |

32 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 33 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim 34 | ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 35 | aliquip ex ea commodo consequat. Duis aute irure dolor in 36 | reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla 37 | pariatur. Excepteur sint occaecat cupidatat non proident, sunt in 38 | culpa qui officia deserunt mollit anim id est laborum 39 |

40 |
41 |
42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/app/api/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/db"; 2 | 3 | export async function GET() { 4 | try { 5 | const data = await db.blog.findMany({ 6 | take: 10, 7 | select: { title: true, category: true, slug: true }, 8 | orderBy: [{ view_count: "desc" }], 9 | }); 10 | 11 | return Response.json(data); 12 | } catch (error) { 13 | console.error("Database Error...", error); 14 | throw new Error("Failed to fetch the popular posts"); 15 | } 16 | } 17 | 18 | export async function POST(request: Request) { 19 | const { slug, title, category } = await request.json(); 20 | 21 | try { 22 | const existingPost = await db.blog.findUnique({ 23 | where: { slug: slug }, 24 | }); 25 | 26 | if (existingPost) { 27 | await db.blog.update({ 28 | where: { slug: slug }, 29 | data: { 30 | view_count: { increment: 1 }, 31 | }, 32 | }); 33 | } else { 34 | await db.blog.create({ 35 | data: { 36 | slug: slug, 37 | title: title, 38 | category: category, 39 | }, 40 | }); 41 | } 42 | } catch (error) { 43 | console.error("Error updating page view", error); 44 | return new Response("Failed to post to DB", { status: 500 }); 45 | } 46 | 47 | return new Response("Successfully posted to DB", { status: 200 }); 48 | } 49 | -------------------------------------------------------------------------------- /src/app/blog/[category]/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | import { formatDate, getBlogPosts } from "../../utils"; 3 | import Header from "@/components/Header"; 4 | import Container from "@/components/Container"; 5 | import { BreadcrumbWithCustomSeparator } from "@/components/Breadcrumb"; 6 | import { CustomMDX } from "@/components/mdx"; 7 | import ReportViews from "@/components/ReportViews"; 8 | import { baseUrl } from "@/app/sitemap"; 9 | 10 | export async function generateStaticParams() { 11 | let posts = getBlogPosts(); 12 | 13 | return posts.map((post) => ({ 14 | slug: post.slug, 15 | })); 16 | } 17 | 18 | export function generateMetadata({ 19 | params, 20 | }: { 21 | params: { slug: string; category: string }; 22 | }) { 23 | let post = getBlogPosts().find((post) => post.slug === params.slug); 24 | if (!post) { 25 | return; 26 | } 27 | 28 | let { 29 | title, 30 | publishedAt: publishedTime, 31 | summary: description, 32 | image, 33 | } = post.metadata; 34 | 35 | let ogImage = image 36 | ? image 37 | : `${baseUrl}/og?title=${encodeURIComponent(title)}`; 38 | 39 | return { 40 | title, 41 | description, 42 | openGraph: { 43 | title, 44 | description, 45 | type: "article", 46 | publishedTime, 47 | url: `${baseUrl}/blog/${post?.metadata.category}/${post?.slug}}`, 48 | images: [{ url: ogImage }], 49 | }, 50 | twitter: { 51 | card: "summary_large_image", 52 | title, 53 | description, 54 | images: [ogImage], 55 | }, 56 | }; 57 | } 58 | 59 | export default function Page({ 60 | params, 61 | }: { 62 | params: { category: string; slug: string }; 63 | }) { 64 | let post = getBlogPosts().find((post) => post.slug === params.slug); 65 | 66 | if (!post) { 67 | notFound(); 68 | } 69 | 70 | return ( 71 | <> 72 |