├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── blog │ ├── [slug] │ │ └── page.tsx │ ├── categories │ │ └── [slug] │ │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.tsx └── page.tsx ├── components.json ├── components └── blog.tsx ├── lib ├── blog.constants.ts ├── cms.ts ├── dates.ts └── utils.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | 2 | # Get your blog id from https://zenblog.com/ > your blog > settings 3 | ZENBLOG_BLOG_ID=ABC -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | !.env.example 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | 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. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /app/blog/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import { cms } from "@/lib/cms"; 3 | import { 4 | PostCategory, 5 | PostDescription, 6 | PostPublishedAt, 7 | PostTitle, 8 | } from "@/components/blog"; 9 | 10 | export default async function BlogPostPage(props: { 11 | params: Promise<{ slug: string }>; 12 | }) { 13 | const params = await props.params; 14 | const { data: post } = await cms.posts.get({ slug: params.slug }); 15 | 16 | if (!post) { 17 | return
Post not found
; 18 | } 19 | 20 | return ( 21 |
22 |
23 |
24 | {post.category ? : null} 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | {post.title} 39 |
40 |
41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /app/blog/categories/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | PostImage, 3 | PostItem, 4 | PostPublishedAt, 5 | PostTitle, 6 | } from "@/components/blog"; 7 | import { cms } from "@/lib/cms"; 8 | import Link from "next/link"; 9 | 10 | export default async function CategoryPage(props: { 11 | params: Promise<{ slug: string }>; 12 | }) { 13 | const params = await props.params; 14 | const { data: posts } = await cms.posts.list({ 15 | limit: 50, 16 | offset: 0, 17 | category: params.slug, 18 | }); 19 | 20 | const firstPost = posts[0]; 21 | 22 | if (!firstPost) { 23 | return ( 24 |
25 |

😅 No posts in this category yet

26 | 27 | Back to all posts 28 | 29 |
30 | ); 31 | } 32 | 33 | return ( 34 |
35 |
36 |

37 | {firstPost.category?.name} 38 |

39 |
40 |
41 | {posts.map((post) => ( 42 | 43 | 44 | 45 | 46 | 47 | ))} 48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/blog/layout.tsx: -------------------------------------------------------------------------------- 1 | import { BLOG_TITLE } from "@/lib/blog.constants"; 2 | import Link from "next/link"; 3 | 4 | export default function BlogLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | return ( 10 |
11 |
12 | 18 | {children} 19 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/blog/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import { cms } from "@/lib/cms"; 3 | import Link from "next/link"; 4 | import { 5 | PostAuthor, 6 | PostCategory, 7 | PostDescription, 8 | PostImage, 9 | PostPublishedAt, 10 | PostTitle, 11 | } from "@/components/blog"; 12 | 13 | // Next.js will invalidate the cache when a 14 | // request comes in, at most once every 5 minutes. 15 | export const revalidate = 300; 16 | 17 | export default async function BlogPage() { 18 | const posts = await cms.posts.list(); 19 | const { data: categories } = await cms.categories.list(); 20 | const lastPost = posts.data.sort((a, b) => { 21 | return ( 22 | new Date(b.published_at).getTime() - new Date(a.published_at).getTime() 23 | ); 24 | })[0]; 25 | const postsWithoutLast = posts.data.filter( 26 | (post) => post.slug !== lastPost.slug 27 | ); 28 | 29 | return ( 30 |
31 |
32 |

Blog

33 |

34 | Stay up to date with the latest news and updates from the team! 35 |

36 |
37 |
38 | 42 | 43 | 44 |
45 |
46 | {lastPost.category?.slug ? ( 47 | 48 | ) : null} 49 | 50 |
51 | 52 | 53 | 54 |
55 | {lastPost.authors?.map((author) => ( 56 | 57 | ))} 58 |
59 | 60 |
61 |
62 | 63 |
64 | 76 |
77 |
78 | {postsWithoutLast.map((post) => ( 79 |
80 | 81 | {post.title} 88 | 89 |
90 |
91 | {post.category ? ( 92 | 93 | ) : null} 94 | 95 |
96 | 97 |

{post.title}

98 | 99 | 100 |
101 |
102 |
103 | {lastPost.authors?.map((author) => ( 104 | 105 | ))} 106 |
107 |
108 |
109 | ))} 110 |
111 |
112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenbloghq/nextjs/588e443a33acc3bf2e4017b4310478a7ca0ba14a/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenbloghq/nextjs/588e443a33acc3bf2e4017b4310478a7ca0ba14a/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenbloghq/nextjs/588e443a33acc3bf2e4017b4310478a7ca0ba14a/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import { BLOG_LANG, BLOG_DESCRIPTION, BLOG_TITLE } from "@/lib/blog.constants"; 4 | 5 | export const metadata: Metadata = { 6 | title: BLOG_TITLE, 7 | description: BLOG_DESCRIPTION, 8 | }; 9 | 10 | export default function RootLayout({ 11 | children, 12 | }: Readonly<{ 13 | children: React.ReactNode; 14 | }>) { 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | export default function Home() { 4 | redirect("/blog"); 5 | } 6 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /components/blog.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import { formatDate } from "@/lib/dates"; 3 | import Link from "next/link"; 4 | import { cn } from "@/lib/utils"; 5 | import { cva } from "class-variance-authority"; 6 | import { Author, Category } from "zenblog/types"; 7 | 8 | export function PostItem({ 9 | children, 10 | className, 11 | slug, 12 | }: { 13 | children: React.ReactNode; 14 | className?: string; 15 | slug: string; 16 | }) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | 24 | export function PostImage({ 25 | src, 26 | alt, 27 | className, 28 | }: { 29 | src?: string; 30 | alt?: string; 31 | className?: string; 32 | }) { 33 | if (!src) { 34 | return
; 35 | } 36 | 37 | return ( 38 |
39 | {alt} 48 |
49 | ); 50 | } 51 | 52 | export function PostPublishedAt({ 53 | publishedAt, 54 | className, 55 | }: { 56 | publishedAt: string; 57 | className?: string; 58 | }) { 59 | return ( 60 | 63 | ); 64 | } 65 | 66 | export function PostCategory({ 67 | category, 68 | className, 69 | }: { 70 | category: Category; 71 | className?: string; 72 | }) { 73 | return ( 74 | 78 | {category.name} 79 | 80 | ); 81 | } 82 | 83 | export function PostDescription({ 84 | description, 85 | className, 86 | }: { 87 | description: string; 88 | className?: string; 89 | }) { 90 | return ( 91 |

92 | {description} 93 |

94 | ); 95 | } 96 | 97 | export function PostTitle({ 98 | title, 99 | className, 100 | size, 101 | as, 102 | }: { 103 | title: string; 104 | as: "h1" | "h2"; 105 | className?: string; 106 | size?: "default" | "lg" | "xl"; 107 | }) { 108 | const titleSizes = cva("font-bold tracking-tighter", { 109 | variants: { 110 | size: { 111 | default: "text-2xl md:text-3xl", 112 | lg: "text-3xl md:text-4xl", 113 | xl: "text-4xl md:text-6xl", 114 | }, 115 | }, 116 | defaultVariants: { 117 | size: "default", 118 | }, 119 | }); 120 | 121 | const Tag = as; 122 | 123 | return {title}; 124 | } 125 | 126 | export function PostAuthor({ 127 | author, 128 | className, 129 | }: { 130 | author: Author; 131 | className?: string; 132 | }) { 133 | return ( 134 |
140 | {author.name} 147 | {author.name} 148 |
149 | ); 150 | } 151 | -------------------------------------------------------------------------------- /lib/blog.constants.ts: -------------------------------------------------------------------------------- 1 | export const BLOG_TITLE = "Next.js Blog"; 2 | export const BLOG_DESCRIPTION = "Powered by Zenblog.com"; 3 | export const BLOG_LANG = "en"; 4 | -------------------------------------------------------------------------------- /lib/cms.ts: -------------------------------------------------------------------------------- 1 | import { createZenblogClient } from "zenblog"; 2 | 3 | const blogId = process.env.ZENBLOG_BLOG_ID; 4 | 5 | if (!blogId) { 6 | throw new Error( 7 | "ZENBLOG_BLOG_ID must be set. Get it from zenblog.com and set it in the .env file." 8 | ); 9 | } 10 | 11 | export const cms = createZenblogClient({ 12 | blogId, 13 | }); 14 | -------------------------------------------------------------------------------- /lib/dates.ts: -------------------------------------------------------------------------------- 1 | export function formatDate(date: string) { 2 | return new Date(date).toLocaleDateString("en-US", { 3 | year: "numeric", 4 | month: "long", 5 | day: "numeric", 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-icons": "^1.3.1", 13 | "class-variance-authority": "^0.7.0", 14 | "clsx": "^2.1.1", 15 | "lucide-react": "^0.454.0", 16 | "next": "15.0.2", 17 | "react": "^18.0.0", 18 | "react-dom": "^18.0.0", 19 | "tailwind-merge": "^2.5.4", 20 | "tailwindcss-animate": "^1.0.7", 21 | "zenblog": "^0.7.5" 22 | }, 23 | "devDependencies": { 24 | "@tailwindcss/typography": "^0.5.15", 25 | "@types/node": "^20", 26 | "@types/react": "^18", 27 | "@types/react-dom": "^18", 28 | "eslint": "^8", 29 | "eslint-config-next": "15.0.2", 30 | "postcss": "^8", 31 | "tailwindcss": "^3.4.1", 32 | "typescript": "^5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import typography from "@tailwindcss/typography"; 3 | import animate from "tailwindcss-animate"; 4 | 5 | const config: Config = { 6 | darkMode: ["class"], 7 | content: [ 8 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 10 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 11 | ], 12 | theme: {}, 13 | plugins: [typography, animate], 14 | }; 15 | export default config; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------