├── .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 |
31 |
32 |
39 |
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 |
65 | {categories.map((category) => (
66 | -
67 |
71 | {category.name}
72 |
73 |
74 | ))}
75 |
76 |
77 |
78 | {postsWithoutLast.map((post) => (
79 |
80 |
81 |
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 |

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 |
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 |
--------------------------------------------------------------------------------