🫱Hướng dẫn Next.js (Full Step)
https://github.com/lpredrum136/nextjs-tutorial-crash
Last updated
Was this helpful?
https://github.com/lpredrum136/nextjs-tutorial-crash
Last updated
Was this helpful?
Mặc định khi sử dụng câu lệnh yarn create next-app ... nó không tạo ra folder pages
Ta đã biết trong app không sử đụng được các function như getInitialProps, getStaticProps, getStaticPaths, getServersideProps 😒 đọc thêm cách sử dụng generateStaticParams
Trong pages
thư mục, getServerSideProps
được sử dụng để lấy dữ liệu trên máy chủ và chuyển tiếp props đến thành phần React được xuất mặc định trong tệp. HTML ban đầu cho trang được kết xuất trước từ máy chủ, sau đó "hydrat hóa" trang trong trình duyệt (làm cho trang có tính tương tác).
pages/dashboard.js
// `pages` directory
export async function getServerSideProps() {
const res = await fetch(`https://...`)
const projects = await res.json()
return { props: { projects } }
}
export default function Dashboard({ projects }) {
return (
<ul>
{projects.map((project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
)
}
Nói chung làm chúng ta có thể sử dụng trong pages như bản cũ 😁
pages\test.tsx
import Link from 'next/link'
type Post = {
"userId": number,
"id": number,
"title": string,
"body": string
}
type Posts = {
posts: Post[]
};
function PostList({ posts }:Posts) {
return (
<>
<h1>List of Posts 1</h1>
{posts.map((post:Post) => {
return (
<div key={post.id}>
<Link href={`posts/${post.id}`}>
<h2>
{post.id} {post.title}
</h2>
</Link>
<hr />
</div>
)
})}
</>
)
}
export default PostList;
export async function getStaticProps() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
const data = await response.json()
return {
props: {
posts: data
}
}
}
pages\lionel\index.tsx
const A = () => {
return (
<div>
aaaaaa 1
</div>
);
}
export default A;
pages\lionel[id].tsx
import { useRouter } from 'next/router';
import { GetStaticPaths, GetStaticProps } from 'next';
type P = {
"userId": number,
"id": number|string,
"title": string,
"body": string
}
type Post = {
post: P
}
function PostId({ post }: Post) {
return (
<>
<h1>List of Posts</h1>
<h2> {post.id} {post.title}</h2>
</>
)
}
export default PostId;
export async function getStaticProps(ctx:any) {
const id = ctx.params.id as string;
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
const data = await response.json()
return {
props: {
post: data
}
}
}
export const getStaticPaths: GetStaticPaths = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();
const paths = posts.map((post: P) => ({
params: {
id: post.id.toString()
},
}))
return {
fallback: false,
paths,
};
};
Sử dụng các function ở client (front-end) như useState, useEffect ... chúng ta phải sử dụng từ khóa đặc biệt 'use client' thì trong back-end nó mới hoạt động được
app\blog\page.tsx
'use client'
import Link from 'next/link';
type Post = {
"userId": number,
"id": number,
"title": string,
"body": string
}
import { useState, useEffect } from 'react'
export default function Page() {
const [posts, setPosts] = useState([]);
useEffect(() => {
async function fetchPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await res.json();
setPosts(data);
}
fetchPosts();
}, []);
if (!posts) return <div>Loading...</div>
return (
<>
<h1>List of Posts 1</h1>
{posts.map((post: Post) => {
return (
<div key={post.id}>
<Link href={`posts/${post.id}`}>
<h2>
{post.id} {post.title}
</h2>
</Link>
<hr />
</div>
)
})}
</>
)
}
Điều đặc biệt back-end nó cũng chỉ là dạng xây dựng Dom
app\posts\page.tsx
const A = () => {
return (
<div>
bbbb 1
</div>
);
}
export default A;
app\posts[id]\page.tsx
const A = () => {
return (
<div>
bbbb 2
</div>
);
}
export default A;
Hỗ trợ lấy routes server
// app/product/[id]/page.tsx
type ProductPageProps = {
params: { id: string };
};
export default function ProductPage({ params }: ProductPageProps) {
return (
<div>
<h2>Product ID: {params.id}</h2>
<p>This is a dynamic route.</p>
</div>
);
}
Cách cũ dùng ở front-end như này nhưng do trong app không dùng 4 function getInitialProps, getStaticProps, getStaticPaths, getServersideProps 😒 đọc thêm cách sử dụng generateStaticParams
app\page.tsx
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
Home
</main>
</div>
);
}
app\layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({children}: Readonly<{children: React.ReactNode;}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{children}
</body>
</html>
);
}
File page nếu chưa cài đặt thay đổi gì nó mặc định sẽ là Page.tsx (Nếu tôi có viết thêm file index.tsx trong đó nó cũng không nhận)
app\blog\page.tsx
export default function Blog() {
return (
<div className="grid">
<main className="flex">
Blog
</main>
</div>
);
}
app\layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({children}: Readonly<{children: React.ReactNode;}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
aaaaaaaaa1
{children}
</body>
</html>
);
}
Và bây giờ như đúng tiên tri nó đã ghi đề
app\blog\layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
aaaaaaaaa2
{children}
</body>
</html>
);
}
https://stackoverflow.com/questions/76267351/how-to-fetch-data-server-side-in-the-latest-next-js-tried-getstaticprops-but-it
lib\post.jsx
import axios from 'axios';
export const getPosts = async limit => {
try {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/posts?_limit=${limit}`
)
return response.data
} catch (error) {
console.log(error)
}
}
app\posts\page.jsx
import { getPosts } from '../../lib/post';
import Link from 'next/link';
export default async function Posts() {
const posts = await getAllPosts();
var allpost = posts.map(post => (
<div className='cardbody shadow p-2 m-2' key={post.id}>
<h1 className="cardtitle">ID: {post.id}</h1>
<h2 className="cardtitle">Title {post.title}</h2>
<h3 className="cardbody">Body: {post.body}</h3>
<Link href={`/posts/${post.id}`}>See more</Link>
</div>
))
return (
<div className="grid">
{allpost}
</div>
);
}
async function getAllPosts() {
const posts = await getPosts(10);
return posts;
}
// app/product/[id]/page.tsx
type ProductPageProps = {
params: { id: string };
};
export default function ProductPage({ params }: ProductPageProps) {
return (
<div>
<h2>Product ID: {params.id}</h2>
<p>This is a dynamic route.</p>
</div>
);
}
app\products[id]\page.tsx
export interface Product {
params: {
id: string
}
}
export default async function Product({ params }: Product) {
const { id } = await params;
return <h1>Product: {id}</h1>;
}
app\products[id]\layout.tsx
export default function ProductLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
{children}
<div>
<h2>Featured products section</h2>
</div>
</div>
);
}
app\api\hello\route.ts
// app/api/hello/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ message: 'Hello from Next.js API!' });
}
https://64ec866af9b2b70f2bfa7c90.mockapi.io/users
app\mock-users\page.tsx
import { revalidatePath } from "next/cache";
type MockUser = {
id: number;
name: string;
};
export default async function Users() {
const res = await fetch("https://64ec866af9b2b70f2bfa7c90.mockapi.io/users");
const users = await res.json();
async function addUser(formData: FormData) {
"use server";
const name = formData.get("name");
const res = await fetch(
"https://64ec866af9b2b70f2bfa7c90.mockapi.io/users",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name }),
}
);
const newUser = await res.json();
console.log(newUser);
revalidatePath("/mock-users");
}
return (
<div className="py-10">
<form action={addUser} className="mb-4">
<input
type="text"
name="name"
required
className="p-2 mr-2 border border-gray-300 rounded text-gray-700"
/>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Add User
</button>
</form>
<div className="grid grid-cols-4 gap-4 ">
{users.map((user: MockUser) => (
<div
key={user.id}
className="p-4 bg-white shadow-md rounded-lg text-gray-700"
>
{user.name}
</div>
))}
</div>
</div>
);
}
app\work\page.tsx
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Divider from '@mui/material/Divider';
import workData from './work.json';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
export default function Work() {
const workList = workData?.work;
const formatDate = (date: string) => {
const dateFormat = new Date(date);
return dateFormat.toLocaleString('en-us', {
year: 'numeric',
month: 'short',
});
};
return (
<>
<Typography variant="h1">Resume</Typography>
<Box sx={{ width: '100%' }}>
<Stack
direction={'column'}
spacing={{ xs: 1, sm: 2, md: 4 }}
divider={<Divider orientation="horizontal" flexItem />}
sx={{ paddingLeft: '0px', alignItems: 'flex-start' }}
>
{workList.map((work, index) => {
const showDivider = index !== workList.length - 1;
const startDate = formatDate(work.startDate);
const endDate = work.endDate ? formatDate(work.endDate) : 'Present';
return (
<div key={index}>
<Typography variant="h2">{work.company}</Typography>
<Typography variant="h3">{work.title}</Typography>
<Typography variant="subtitle1" gutterBottom>
{startDate + ' - ' + endDate}
</Typography>
<Typography variant="body1" sx={{ margin: '10px 0 10px 0' }}>
{work.description}
</Typography>
<List sx={{ listStyleType: 'disc', listStylePosition: 'inside' }}>
{work.notes.map((note, index) => {
return (
<ListItem sx={{ display: 'list-item' }} key={index}>
{note}
</ListItem>
);
})}
</List>
</div>
)
})}
</Stack>
</Box>
</>
);
}
app\work\work.json
{
"work": [
{
"company": "Atlassian",
"title": "Software Engineer",
"status": "Full-time",
"location": "remote",
"startDate": "7/1/20",
"endDate": "",
"description": "I joined Atlassian's Confluence team and a new grad software engineer eager to tackle frontend development on an awesome product.",
"notes": [
"Worked on the core product of Confluence for 2020-2022 years, building collaborative tools and components. Engaged with a large team of engineers to develop projects spanning months to years long, contributing to platform components, integrating with microservices, frequent cross-team collaboration, ownership and maintenance of major elements on Confluence with on-call rotations.",
"For the past year I have worked on a Confluence team running experiments for landing and onboarding customers. We build smaller scale projects - collaborating with PMs, designs, architects, other teams, and data analysts to rapidly design, develop, and analyze the impact of experiments."
],
"relavantLinks": []
},
{
"company": "SJSU Research Foundation",
"title": "STEM Mentor",
"status": "Part-time",
"location": "San Jose, CA",
"startDate": "8/1/18",
"endDate": "1/1/20",
"description": "I offered high school students mentorship in their science classes.",
"notes": [
"I aided in teaching the scientific method, teaching students how to use autocad for 3D printing, and assisting in their experimental process."
],
"relavantLinks": []
},
{
"company": "Google",
"title": "Developer Relations Intern",
"status": "Full-time",
"location": "Sunnyvale, CA",
"startDate": "5/1/19",
"endDate": "9/1/19",
"description": "Interned a second year on the Angular team to build an ecosystem housing useful tooling and technologies for building Angular apps",
"notes": [
"After interning for Angular in summer 2018 and then contributing to the community during the rest of the year, I came back to Angular for summer 2019 to build upon a larger project of evailing useful tools and technologies for Angular.",
"Developed a search engine for Angular tooling that allows you to pick and choose the best tools/components for your app. - ellamaolson/ngEcosystem",
"Created a guide for Angular beginners - How to build an Angular app with custom components, routing, services, forms, and Angular Material in 6 steps."
],
"relavantLinks": [
"https://medium.com/@elanaolson/create-an-angular-app-in-no-time-2ca89c734dd2",
"https://github.com/ellamaolson/ngEcosystem"
]
},
{
"company": "Google",
"title": "Developer Relations Intern",
"status": "Full-time",
"location": "Mountain View, CA",
"startDate": "5/1/18",
"endDate": "9/1/18",
"description": "Interned on the Angular team to develop and share tooling, documentation, and guides on migrating from AngularJS to Angular",
"notes": [
"Having never worked in Angular before, I eagerly dove in to learn how the community was using Angular and quickly discovered many people struggling with migrating from AngularJS to Angular v2+. I worked closely with major customers to devise a list of pain points they were experiencing in this migration process and built a migration tool to scan, parse, and recommend how to migrate their applications.",
"ngMigration-Assistant is an analysis tool that recommends a custom migration path to take to Angular. It scans an AngularJS application for AngularJS patterns, provides statistics on your application, and identify the necessary steps for migrating to Angular. It highlights files containing AngularJS patterns and directs on how to convert those patterns to be Angular compatible. It is the first step in starting a your migration from AngularJS to Angular.",
"In the process, I shared this tool and my learnings with the larger Angular community - speaking at local and international meetups and conferences and wrote a detailed blog announcing the tool.",
"We created a community hub, ngMigration Forum, for discussing, learning, and sharing relevant information on migrating. So many businesses were going through the same experience and creating private internal documentation I saw duplicated across many teams, so a centralized place felt necessary."
],
"relavantLinks": [
"https://blog.angular.io/migrating-to-angular-fc9618d6fb04",
"https://github.com/ellamaolson/ngMigration-Assistant",
"https://github.com/angular/ngMigration-Forum"
]
},
{
"company": "Tappity",
"title": "Frontend Engineering Intern",
"status": "Full-time",
"location": "Palo Alto, CA",
"startDate": "1/3/17",
"endDate": "11/1/17",
"description": "Learned and built a website for Tappity using JavaScript, HTML, and Bootstrap CSS",
"notes": [],
"relavantLinks": []
}
]
}
types.d.ts
type Post = {
"userId": number,
"id": number,
"title": string,
"body": string,
}
type User = {
"id": number,
"name": string,
"username": string,
"email": string,
"address": {
"street": string,
"suite": string,
"city": string,
"zipcode": string,
"geo": {
"lat": string,
"lng": string
}
},
"phone": string,
"website": string,
"company": {
"name": string,
"catchPhrase": string,
"bs": string
}
}
lib\getAllUsers.ts
export default async function getAllUsers() {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
if (!res.ok) throw new Error('failed to fetch data')
return res.json()
}
app\users\page.tsx
import getAllUsers from '@/lib/getAllUsers'
import Link from "next/link"
export default async function UsersPage() {
const usersData: Promise<any[]> = getAllUsers()
const users = await usersData
const content = (
<section>
<h2>
<Link href="/">Back to Home</Link>
</h2>
{users.map(user => {
return (
<div key={user.id}>
<Link href={`/users/${user.id}`}>{user.name}</Link>
</div>
)
})}
</section>
)
return content
}
app\users[userId]\page.tsx
import getUser from "@/lib/getUser"
import getUserPosts from "@/lib/getUserPosts"
import { Suspense } from "react"
import UserPosts from "./components/UserPosts"
import type { Metadata } from 'next'
type Params = {
params: {
userId: string
}
}
export async function generateMetadata({ params: { userId } }: Params): Promise<Metadata> {
const userData: Promise<User> = getUser(userId)
const user: User = await userData
return {
title: user.name,
description: `This is the page of ${user.name}`
}
}
export default async function UserPage({ params: { userId } }: Params) {
const userData: Promise<User> = getUser(userId)
const userPostsData: Promise<Post[]> = getUserPosts(userId)
// If not progressively rendering with Suspense, use Promise.all
//const [user, userPosts] = await Promise.all([userData, userPostsData])
const user = await userData
return (
<>
<h2>{user.name}</h2>
<br />
<Suspense fallback={<h2>Loading...</h2>}>
<UserPosts promise={userPostsData} />
</Suspense>
</>
)
}
lib\getUserPosts.ts
export default async function getUserPosts(userId: string) {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`)
if (!res.ok) throw new Error('failed to fetch user')
return res.json()
}
app\users[userId]\components\UserPosts.tsx
type Props = {
promise: Promise<Post[]>
}
export default async function UserPosts({ promise }: Props) {
const posts = await promise;
const content = posts.map(post => {
return (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
<br />
</article>
)
})
return content
}
lib\getUser.ts
export default async function getUser(userId: string) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
if (!res.ok) throw new Error('failed to fetch user')
return res.json()
}
app\layout.tsx
import "./globals.css";
import Navbar from './components/Navbar';
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) {
return (
<html lang="en">
<body className="antialiased">
<Navbar />
{children}
</body>
</html>
);
}
app\posts[postId]\page.tsx
import getFormattedDate from "@/lib/getFormattedDate";
import { getSortedPostsData, getPostData } from "@/lib/posts"
import { notFound } from "next/navigation"
import Link from "next/link"
export function generateStaticParams() {
const posts = getSortedPostsData()
return posts.map((post) => ({
postId: post.id
}))
}
export default async function Post({ params }: { params: { postId: string } }) {
const posts = getSortedPostsData()
const { postId } = await params;
if (!posts.find(post => post.id === postId)) notFound();
const { title, date, contentHtml } = await getPostData(postId)
const pubDate = getFormattedDate(date)
return (
<main className="px-6 prose prose-xl prose-slate dark:prose-invert mx-auto">
<h1 className="text-3xl mt-4 mb-0">{title}</h1>
<p className="mt-0">
{pubDate}
</p>
<article>
<section dangerouslySetInnerHTML={{ __html: contentHtml }} />
<p>
<Link href="/">← Back to home</Link>
</p>
</article>
</main>
)
}
app\components\Posts.tsx
import { getSortedPostsData } from "@/lib/posts";
import ListItem from "./ListItem";
export default function Posts() {
const posts = getSortedPostsData()
return (
<section className="mt-6 mx-auto max-w-2xl">
<h2 className="text-4xl font-bold dark:text-white/90">Blog</h2>
<ul className="w-full">
{posts.map(post => (
<ListItem key={post.id} post={post} />
))}
</ul>
</section>
)
}
app\components\Navbar.tsx
import Link from "next/link"
import { FaYoutube, FaTwitter, FaGithub, FaLaptop } from "react-icons/fa"
export default function Navbar() {
return (
<nav className="bg-slate-600 p-4 sticky top-0 drop-shadow-xl z-10">
<div className="prose prose-xl mx-auto flex justify-between flex-col sm:flex-row">
<h1 className="text-3xl font-bold text-white grid place-content-center mb-2 md:mb-0">
<Link href="/" className="text-white/90 no-underline hover:text-white">Dave Gray</Link>
</h1>
<div className="flex flex-row justify-center sm:justify-evenly align-middle gap-4 text-white text-4xl lg:text-5xl">
<Link className="text-white/90 hover:text-white" href="/posts/pre-rendering">
Pre-rendering
</Link>
<Link className="text-white/90 hover:text-white" href="/posts/ssg-ssr">
Ssg-ssr
</Link>
<Link className="text-white/90 hover:text-white" href="https://www.youtube.com/@DaveGrayTeachesCode">
<FaYoutube />
</Link>
<Link className="text-white/90 hover:text-white" href="https://courses.davegray.codes/">
<FaLaptop />
</Link>
<Link className="text-white/90 hover:text-white" href="https://github.com/gitdagray">
<FaGithub />
</Link>
<Link className="text-white/90 hover:text-white" href="https://twitter.com/yesdavidgray">
<FaTwitter />
</Link>
</div>
</div>
</nav>
)
}
app\components\ListItem.tsx
import Link from "next/link"
import getFormattedDate from "@/lib/getFormattedDate"
type Props = {
post: BlogPost
}
export default function ListItem({ post }: Props) {
const { id, title, date } = post;
const formattedDate = getFormattedDate(date)
return (
<li className="mt-4 text-2xl dark:text-white/90">
<Link className="underline hover:text-black/70 dark:hover:text-white" href={`/posts/${id}`}>{title}</Link>
<br />
<p className="text-sm mt-1">{formattedDate}</p>
</li>
)
}
blogposts\pre-rendering.md
---
title: "Two Forms of Pre-rendering"
date: "2023-03-14"
---
Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
blogposts\ssg-ssr.md
---
title: 'When to Use Static Generation vs. Server-side Rendering'
date: '2023-03-17'
---
We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
You can use Static Generation for many types of pages, including:
- Marketing pages
- Blog posts
- E-commerce product listings
- Help and documentation
You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
app\api\echo\route.ts
import { NextResponse } from "next/server"
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
//const name = searchParams.get('name')
//const instrument = searchParams.get('instrument')
const obj = Object.fromEntries(searchParams.entries())
//return NextResponse.json({ name, instrument })
return NextResponse.json(obj)
}
app\api\feedback\route.ts
import { NextResponse } from "next/server"
type Feedback = {
name?: string,
email?: string,
message?: string,
}
export async function POST(request: Request) {
const data: Feedback = await request.json()
console.log('data: ', data)
const { name, email, message } = data
return NextResponse.json({ name, email, message })
}
app\feedback\page.tsx
"use client"
import { useState, FormEvent, ChangeEvent } from "react"
import { useRouter } from "next/navigation"
const initState = {
name: "",
email: "",
message: "",
}
export default function Feedback() {
const [data, setData] = useState(initState)
const router = useRouter()
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
console.log(JSON.stringify(data))
const { name, email, message } = data
// Send data to API route
const res = await fetch('http://localhost:3000/api/feedback', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name, email, message
})
})
const result = await res.json()
console.log(result)
// Navigate to thank you
router.push(`/thank-you/`)
}
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const name = e.target.name
setData(prevData => ({
...prevData,
[name]: e.target.value
}))
}
const canSave = [...Object.values(data)].every(Boolean)
const content = (
<form onSubmit={handleSubmit} className="flex flex-col mx-auto max-w-3xl p-6">
<h1 className="text-4xl mb-4">Contact Us</h1>
<label className="text-2xl mb-1" htmlFor="name">Name:</label>
<input
className="p-3 mb-6 text-2xl rounded-2xl text-black"
type="text"
id="name"
name="name"
placeholder="Jane"
pattern="([A-Z])[\w+.]{1,}"
value={data.name}
onChange={handleChange}
autoFocus
/>
<label className="text-2xl mb-1" htmlFor="email">Email:</label>
<input
className="p-3 mb-6 text-2xl rounded-2xl text-black"
type="email"
id="email"
name="email"
placeholder="Jane@yoursite.com"
pattern="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
value={data.email}
onChange={handleChange}
/>
<label className="text-2xl mb-1" htmlFor="message">Message:</label>
<textarea
className="p-3 mb-6 text-2xl rounded-2xl text-black"
id="message"
name="message"
placeholder="Your message here..."
rows={5}
cols={33}
value={data.message}
onChange={handleChange}
/>
<button
className="p-3 mb-6 text-2xl rounded-2xl text-black border-solid border-white border-2 max-w-xs bg-slate-400 hover:cursor-pointer hover:bg-slate-300 disabled:hidden"
disabled={!canSave}
>Submit</button>
</form>
)
return content
}
app\thank-you\page.tsx
export default function ThankYou() {
return (
<main>
<h1 className="text-3xl grid place-content-center min-h-screen">
Thank you for your feedback!
</h1>
</main>
)
}
app\api\todos\route.ts
import { NextResponse } from 'next/server'
const DATA_SOURCE_URL = "https://64ec866af9b2b70f2bfa7c90.mockapi.io/users"
const API_KEY: string = process.env.DATA_API_KEY as string
export async function GET() {
const res = await fetch(DATA_SOURCE_URL)
const todos: Todo[] = await res.json()
return NextResponse.json(todos)
}
export async function POST(request: Request) {
const { name }: Partial<Todo> = await request.json()
if (!name) return NextResponse.json({ "message": "Missing required data" })
const res = await fetch(DATA_SOURCE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'API-Key': API_KEY
},
body: JSON.stringify({
name
})
})
const newTodo: Todo = await res.json()
return NextResponse.json(newTodo)
}
export async function PUT(request: Request) {
const { id, name }: Todo = await request.json()
if (!name) return NextResponse.json({ "message": "Missing required data" })
const res = await fetch(`${DATA_SOURCE_URL}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'API-Key': API_KEY
},
body: JSON.stringify({
name
})
})
const updatedTodo: Todo = await res.json()
return NextResponse.json(updatedTodo)
}
export async function DELETE(request: Request) {
const { id }: Partial<Todo> = await request.json()
if (!id) return NextResponse.json({ "message": "Todo id required" })
await fetch(`${DATA_SOURCE_URL}/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'API-Key': API_KEY
}
})
return NextResponse.json({ "message": `Todo ${id} deleted` })
}
types.d.ts
type Post = {
"userId": number,
"id": number,
"title": string,
"body": string,
}
type User = {
"id": number,
"name": string,
"username": string,
"email": string,
"address": {
"street": string,
"suite": string,
"city": string,
"zipcode": string,
"geo": {
"lat": string,
"lng": string
}
},
"phone": string,
"website": string,
"company": {
"name": string,
"catchPhrase": string,
"bs": string
}
}
type Result = {
pageid: string,
title: string,
extract: string,
thumbnail?: {
source: string,
width: number,
height: number,
}
}
type SearchResult = {
query?: {
pages?: Result[],
},
}
type BlogPost = {
id: string,
title: string,
date: string,
}
type Todo = {
id: number
name: string
}
app\api\todos[id]\route.ts
import { NextResponse } from 'next/server'
const DATA_SOURCE_URL = "https://64ec866af9b2b70f2bfa7c90.mockapi.io/users"
export async function GET(request: Request) {
const id = request.url.slice(request.url.lastIndexOf('/') + 1)
const res = await fetch(`${DATA_SOURCE_URL}/${id}`)
const todo: Todo = await res.json()
if (!todo.id) return NextResponse.json({ "message": "Todo not found" })
return NextResponse.json(todo)
}
Trang này đã tạo tài khoản
middleware.ts
import { NextResponse } from "next/server";
const allowedOrigins = process.env.NODE_ENV === 'production' ? ['https://www.yoursite.com', 'https://yoursite.com'] : ['http://localhost:3000']
export function middleware(request: Request) {
const origin = request.headers.get('origin')
console.log("origin:",origin)
if (origin && !allowedOrigins.includes(origin)) {
return new NextResponse(null, {
status: 400,
statusText: "Bad Request",
headers: {
'Content-Type': 'text/plain'
}
})
}
console.log('Middleware!')
console.log(request.method)
console.log(request.url)
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*',
}
app\api\todos\route.ts
import { NextResponse } from 'next/server'
const DATA_SOURCE_URL = "https://64ec866af9b2b70f2bfa7c90.mockapi.io/users"
const API_KEY: string = process.env.DATA_API_KEY as string
export async function GET(request: Request) {
const origin = request.headers.get('origin');
const res = await fetch(DATA_SOURCE_URL)
const todos: Todo[] = await res.json()
return new NextResponse(JSON.stringify(todos), {
headers: {
'Access-Control-Allow-Origin': origin || "*",
'Content-Type': 'application/json',
}
})
}
export async function POST(request: Request) {
const { name }: Partial<Todo> = await request.json()
if (!name) return NextResponse.json({ "message": "Missing required data" })
const res = await fetch(DATA_SOURCE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'API-Key': API_KEY
},
body: JSON.stringify({
name
})
})
const newTodo: Todo = await res.json()
return NextResponse.json(newTodo)
}
export async function PUT(request: Request) {
const { id, name }: Todo = await request.json()
if (!name) return NextResponse.json({ "message": "Missing required data" })
const res = await fetch(`${DATA_SOURCE_URL}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'API-Key': API_KEY
},
body: JSON.stringify({
name
})
})
const updatedTodo: Todo = await res.json()
return NextResponse.json(updatedTodo)
}
export async function DELETE(request: Request) {
const { id }: Partial<Todo> = await request.json()
if (!id) return NextResponse.json({ "message": "Todo id required" })
await fetch(`${DATA_SOURCE_URL}/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'API-Key': API_KEY
}
})
return NextResponse.json({ "message": `Todo ${id} deleted` })
}
app\api\config\limiter.ts
import { RateLimiter } from "limiter";
export const limiter = new RateLimiter({
tokensPerInterval: 3,
interval: "min",
fireImmediately: true,
})
app\api\hello\route.ts
import { NextResponse } from "next/server";
import { limiter } from "../config/limiter";
export async function GET(request: Request) {
const origin = request.headers.get('origin')
const remaining = await limiter.removeTokens(1)
console.log('remaining: ', remaining)
if (remaining < 0) {
return new NextResponse(null, {
status: 429,
statusText: "Too Many Requests",
headers: {
'Access-Control-Allow-Origin': origin || '*',
'Content-Type': 'text/plain',
}
})
}
return new Response('Hello, Next.js!')
}
app\add\page.tsx
import AddTodo from "@/app/components/AddTodo";
export default function page() {
return (
<AddTodo />
)
}
app\components\AddTodo.tsx
"use client"
import { useRouter } from "next/navigation"
import { useState, useTransition, FormEvent, ChangeEvent } from 'react'
import { usePathname } from "next/navigation"
const initState: Partial<Todo> = {
id: 1,
name: "",
}
export default function AddTodo() {
const router = useRouter()
const pathname = usePathname()
const [isPending, startTransition] = useTransition()
const [isFetching, setIsFetching] = useState(false)
const [data, setData] = useState(initState)
const isMutating = isFetching || isPending
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const { id, name } = data
setIsFetching(true)
const res = await fetch(`https://64ec866af9b2b70f2bfa7c90.mockapi.io/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id, name
})
})
await res.json()
setIsFetching(false)
setData(prevData => ({
...prevData,
title: ""
}))
startTransition(() => {
if (pathname === "/add") {
router.push('/')
} else {
// Refresh the current route and fetch new data
// from the server without losing
// client-side browser or React state.
router.refresh()
}
})
}
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const name = e.target.name
setData(prevData => ({
...prevData,
[name]: e.target.value
}))
}
const content = (
<form onSubmit={handleSubmit} className="flex gap-2 items-center" style={{ opacity: !isMutating ? 1 : 0.7 }}>
<input
type="text"
id="name"
name="name"
value={data.name}
onChange={handleChange}
className="text-2xl p-1 rounded-lg flex-grow w-full"
placeholder="New Todo"
autoFocus
/>
<button type="submit" className="p-2 text-xl rounded-2xl text-black border-solid border-black border-2 max-w-xs bg-green-500 hover:cursor-pointer hover:bg-green-400">
Submit
</button>
</form>
)
return content
}
app\components\Navbar.tsx
import Link from "next/link"
export default function Navbar() {
return (
<nav className="bg-slate-600 p-4 sticky top-0 drop-shadow-xl z-10">
<div className="max-w-xl mx-auto sm:px-4 flex justify-between">
<h1 className="text-3xl font-bold mb-0">
<Link href="/" className="text-white/90 no-underline hover:text-white">Next Todos</Link>
</h1>
<Link href="/add" className="text-2xl text-white/90 no-underline hover:text-white">Add</Link>
</div>
</nav>
)
}
app\components\Todo.tsx
"use client"
import { FaTrash, FaPen } from "react-icons/fa"
import { useRouter } from 'next/navigation'
import { useState, useTransition, ChangeEvent, MouseEvent } from 'react'
import Link from "next/link";
const initState: Partial<Todo> = {
name: "",
}
export default function Todo(todo: Todo) {
const router = useRouter()
const [isPending, startTransition] = useTransition()
const [isFetching, setIsFetching] = useState(false)
const isMutating = isFetching || isPending;
const [data, setData] = useState(initState)
const handleUpdate = async (e: MouseEvent<HTMLButtonElement>) => {
setIsFetching(true)
const res = await fetch(`https://64ec866af9b2b70f2bfa7c90.mockapi.io/users/${todo.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...data
})
})
await res.json()
setIsFetching(false)
startTransition(() => {
// Refresh the current route and fetch new data
// from the server without losing
// client-side browser or React state.
router.refresh()
})
}
const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
const name = e.target.name;
setData((prevData: any) => ({
...prevData,
[name]: e.target.value
}));
}
const handleDelete = async (e: MouseEvent<HTMLButtonElement>) => {
setIsFetching(true)
const res = await fetch(`https://64ec866af9b2b70f2bfa7c90.mockapi.io/users/${todo.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: todo.id
})
})
await res.json()
setIsFetching(false)
startTransition(() => {
// Refresh the current route and fetch new data
// from the server without losing
// client-side browser or React state.
router.refresh()
})
}
return (
<article className="my-4 flex justify-between items-center" style={{ opacity: !isMutating ? 1 : 0.7 }}>
<label className="text-2xl hover:underline">
<Link href={`/edit/${todo.id}`}>{todo.name}</Link>
</label>
<div className="flex items-center gap-4">
<input
type="text"
id="name"
name="name"
value={data.name === "" ? todo.name : data.name}
onChange={handleChange}
className="text-2xl p-1 rounded-lg flex-grow w-full"
placeholder="New Todo"
autoFocus
/>
<button
onClick={handleUpdate}
disabled={isPending}
className="p-3 text-xl rounded-2xl text-black border-solid border-black border-2 max-w-xs bg-red-400 hover:cursor-pointer hover:bg-red-300">
<FaPen />
</button>
<button
onClick={handleDelete}
disabled={isPending}
className="p-3 text-xl rounded-2xl text-black border-solid border-black border-2 max-w-xs bg-red-400 hover:cursor-pointer hover:bg-red-300">
<FaTrash />
</button>
</div>
</article>
)
}
import Todo from "./Todo"
import fetchTodos from "@/lib/fetchTodos"
export default async function TodoList() {
const todos = await fetchTodos()
const sortedTodos = todos.reverse()
const content = (
<>
{sortedTodos.map(todo => (
<Todo key={todo.id} {...todo} />
))}
</>
)
return content
}
app\edit[id]\page.tsx
import Todo from "@/app/components/Todo"
import fetchTodo from "@/lib/fetchTodo"
import { notFound } from "next/navigation"
export const revalidate = 0
type Props = {
params: {
id: string
}
}
export default async function page({ params }: Props) {
var { id } = await params;
const todo = await fetchTodo(id)
if (!todo) notFound()
return (
<Todo {...todo} />
)
}
app\layout.tsx
import Navbar from "./components/Navbar";
import "./globals.css";
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) {
return (
<html lang="en">
<body className="antialiased">
<Navbar />
{children}
</body>
</html>
);
}
app\page.tsx
import TodoList from "./components/TodoList";
import AddTodo from "./components/AddTodo";
export const revalidate = 0
export default function Home() {
return (
<>
<AddTodo />
<TodoList />
</>
)
}
lib\fetchTodo.ts
export default async function fetchTodo(id: string) {
const res = await fetch(`https://64ec866af9b2b70f2bfa7c90.mockapi.io/users/${id}`)
if (!res.ok) return undefined
const todo: Todo = await res.json()
return todo
}
lib\fetchTodos.ts
export default async function fetchTodos() {
const res = await fetch(`https://64ec866af9b2b70f2bfa7c90.mockapi.io/users`)
const todos: Todo[] = await res.json()
return todos
}
// https://<your-site.com>/api/revalidate?secret=<token>
// http://localhost:3000/api/revalidate?path=/&secret=DaveGrayTeachesCode
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
return res.status(401).json({ message: 'Invalid token ' })
}
const path = req.query.path as string
await res.revalidate(path)
return res.json({ revalidated: true })
}
app\api\revalidate\route.ts
import { NextRequest, NextResponse } from 'next/server'
import { revalidatePath } from 'next/cache'
export async function GET(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret');
if (secret !== process.env.MY_SECRET_TOKEN) {
return new NextResponse(JSON.stringify({ message: 'Invalid Token' }), {
status: 401,
statusText: 'Unauthorized',
headers: {
'Content-Type': 'application/json'
}
})
}
const path = request.nextUrl.searchParams.get('path') || '/'
revalidatePath(path)
return NextResponse.json({ revalidated: true })
}
.env
MY_SECRET_TOKEN=5c6be274da094d70b5062694ad588788
GITHUB_TOKEN=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
app\components\CustomImage.tsx
import Image from "next/image"
type Props = {
src: string,
alt: string,
priority?: string,
}
export default function CustomImage({ src, alt, priority }: Props) {
const prty = priority ? true : false
return (
<div className="w-full h-full">
<Image
className="rounded-lg mx-auto"
src={src}
alt={alt}
width={650}
height={650}
priority={prty}
/>
</div>
)
}
app\components\ListItem.tsx
import Link from "next/link"
import getFormattedDate from "@/lib/getFormattedDate"
type Props = {
post: Meta
}
export default function ListItem({ post }: Props) {
const { id, title, date } = post
const formattedDate = getFormattedDate(date)
return (
<li className="mt-4 text-2xl dark:text-white/90">
<Link className="underline hover:text-black/70 dark:hover:text-white" href={`/posts/${id}`}>{title}</Link>
<br />
<p className="text-sm mt-1">{formattedDate}</p>
</li>
)
}
app\components\MyProfilePic.tsx
import Image from "next/image"
export default function MyProfilePic() {
return (
<section className="w-full mx-auto">
<Image
className="border-4 border-black dark:border-slate-500 drop-shadow-xl shadow-black rounded-full mx-auto mt-8"
src="/images/profile-photo-600x600.png"
width={200}
height={200}
alt="Dave Gray"
priority={true}
/>
</section>
)
}
app\components\Navbar.tsx
import Link from "next/link"
import { FaYoutube, FaTwitter, FaGithub, FaLaptop } from "react-icons/fa"
export default function Navbar() {
return (
<nav className="bg-slate-600 p-4 sticky top-0 drop-shadow-xl z-10">
<div className="md:px-6 prose prose-xl mx-auto flex justify-between flex-col sm:flex-row">
<h1 className="text-3xl font-bold text-white grid place-content-center mb-2 md:mb-0">
<Link href="/" className="text-white/90 no-underline hover:text-white">Dave Gray</Link>
</h1>
<div className="flex flex-row justify-center sm:justify-evenly align-middle gap-4 text-white text-4xl lg:text-5xl">
<Link className="text-white/90 hover:text-white" href="https://www.youtube.com/@DaveGrayTeachesCode">
<FaYoutube />
</Link>
<Link className="text-white/90 hover:text-white" href="https://courses.davegray.codes/">
<FaLaptop />
</Link>
<Link className="text-white/90 hover:text-white" href="https://github.com/gitdagray">
<FaGithub />
</Link>
<Link className="text-white/90 hover:text-white" href="https://twitter.com/yesdavidgray">
<FaTwitter />
</Link>
</div>
</div>
</nav>
)
}
app\components\Posts.tsx
import { getPostsMeta } from "@/lib/posts"
import ListItem from "./ListItem"
export default async function Posts() {
const posts = await getPostsMeta()
if (!posts) {
return <p className="mt-10 text-center">Sorry, no posts available.</p>
}
return (
<section className="mt-6 mx-auto max-w-2xl">
<h2 className="text-4xl font-bold dark:text-white/90">Blog</h2>
<ul className="w-full list-none p-0">
{posts.map(post => (
<ListItem key={post.id} post={post} />
))}
</ul>
</section>
)
}
app\components\Video.tsx
type Props = {
id: string
}
export default function Video({ id }: Props) {
return (
<div className="aspect-w-16 aspect-h-9">
<iframe
src={`https://www.youtube.com/embed/${id}`}
title="YouTube video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
/>
</div>
);
}
app\posts[postId]\page.tsx
import getFormattedDate from "@/lib/getFormattedDate"
import { getPostByName } from "@/lib/posts"
import { notFound } from "next/navigation"
import Link from "next/link"
import 'highlight.js/styles/github-dark.css'
export const revalidate = 86400
type Props = {
params: {
postId: string
}
}
export default async function Post({ params }: Props) {
var {postId} = await params;
const post = await getPostByName(`${postId}.mdx`) //deduped!
console.log(post);
if (!post) notFound()
const { meta, content } = post;
const pubDate = getFormattedDate(meta.date);
const tags = meta.tags.map((tag, i) => (
<Link key={i} href={`/tags/${tag}`}>{tag}</Link>
))
return (
<>
<h2 className="text-3xl mt-4 mb-0">{meta.title}</h2>
<p className="mt-0 text-sm">
{pubDate}
</p>
<article>
{content}
</article>
<section>
<h1 className="text-5xl mt-4 mb-0">Related Lionel 👇</h1>
<div className="flex flex-row gap-4">
{tags}
</div>
</section>
<p className="mb-10">
<Link href="/">← Back to home</Link>
</p>
</>
)
}
app\tags[tag]\page.tsx
import { getPostsMeta } from "@/lib/posts"
import ListItem from "@/app/components/ListItem"
import Link from "next/link"
export const revalidate = 86400
type Props = {
params: {
tag: string
}
}
export default async function TagPostList({ params }: Props) {
var {tag} = await params;
const posts = await getPostsMeta() //deduped!
if (!posts) return <p className="mt-10 text-center">Sorry, no posts available.</p>
const tagPosts = posts.filter(post => post.tags.includes(tag))
if (!tagPosts.length) {
return (
<div className="text-center">
<p className="mt-10">Sorry, no posts for that keyword.</p>
<Link href="/">Back to Home</Link>
</div>
)
}
return (
<>
<h2 className="text-3xl mt-4 mb-0">Results for: #{tag}</h2>
<section className="mt-6 mx-auto max-w-2xl">
<ul className="w-full list-none p-0">
{tagPosts.map(post => (
<ListItem key={post.id} post={post} />
))}
</ul>
</section>
</>
)
}
app\layout.tsx
import './globals.css'
import Navbar from './components/Navbar'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: "Dave's Blog",
description: 'Created by Dave Gray',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className="dark:bg-slate-800">
<Navbar />
<main className="px-4 md:px-6 prose prose-xl prose-slate dark:prose-invert mx-auto">
{children}
</main>
</body>
</html>
)
}
app\page.tsx
import Posts from "./components/Posts"
import MyProfilePic from './components/MyProfilePic'
export const revalidate = 86400
export default function Home() {
return (
<div className="mx-auto">
<MyProfilePic />
<p className="mt-12 mb-12 text-3xl text-center dark:text-white">
Hello and Welcome 👋
<span className="whitespace-nowrap">
I'm <span className="font-bold">Dave</span>.
</span>
</p>
<Posts />
</div>
)
}
lib\getFormattedDate.ts
export default function getFormattedDate(dateString: string): string {
return new Intl.DateTimeFormat('vi-VN', { dateStyle: 'long' }).format(new Date(dateString))
}
lib\posts.ts
import { compileMDX } from 'next-mdx-remote/rsc';
import rehypeSlug from 'rehype-slug';
import Video from '@/app/components/Video'
import CustomImage from '@/app/components/CustomImage'
type Filetree = {
"tree": [
{
"path": string,
}
]
}
export async function getPostByName(fileName: string): Promise<BlogPost | undefined> {
const res = await fetch(`https://raw.githubusercontent.com/phamngoctuong/ipa/refs/heads/main/${fileName}`, {
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
'X-GitHub-Api-Version': '2025-11-11',
}
})
if (!res.ok) return undefined
const rawMDX = await res.text()
if (rawMDX === '404: Not Found') return undefined
const { frontmatter, content } = await compileMDX<{ title: string, date: string, tags: string[] }>({
source: rawMDX,
components: {
Video,
CustomImage,
},
options: {
parseFrontmatter: true,
mdxOptions: {
rehypePlugins: [
rehypeSlug
],
},
}
})
const id = fileName.replace(/\.mdx$/, '')
const blogPostObj: BlogPost = { meta: { id, title: frontmatter.title, date: frontmatter.date, tags: frontmatter.tags }, content }
return blogPostObj
}
export async function getPostsMeta(): Promise<Meta[] | undefined> {
const res = await fetch('https://api.github.com/repos/phamngoctuong/ipa/git/trees/main?recursive=1', {
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
// 'X-GitHub-Api-Version': '2024-12-28',
}
})
if (!res.ok) return undefined
const repoFiletree: Filetree = await res.json();
const filesArray = repoFiletree.tree.map(obj => obj.path).filter(path => path.endsWith('.mdx'));
const posts: Meta[] = []
for (const file of filesArray) {
const post = await getPostByName(file);
console.log(post);
if (post) {
const { meta } = post
posts.push(meta)
}
}
return posts.sort((a, b) => a.date < b.date ? 1 : -1)
}
---
title: File shi.mdx
date: '2020-05-05T00:00:00.000Z'
tags: ['shit', 'world']
author: Alexandra Doe
description: This is my shit post
---
Hey this is my second post
# Shit 💩
This is the end of the shit post