การสร้างบล็อกที่มีผู้เขียนหลายคนด้วย Next.js
เผยแพร่แล้ว: 2022-03-10ในบทความนี้ เราจะสร้างบล็อกด้วย Next.js ที่รองรับผู้เขียนตั้งแต่สองคนขึ้นไป เราจะถือว่าแต่ละโพสต์เป็นของผู้เขียนและแสดงชื่อและรูปภาพพร้อมกับโพสต์ของพวกเขา ผู้เขียนแต่ละคนยังได้รับหน้าโปรไฟล์ ซึ่งแสดงรายการโพสต์ทั้งหมดที่พวกเขามีส่วนร่วม มันจะมีลักษณะดังนี้:
เราจะเก็บข้อมูลทั้งหมดในไฟล์บนระบบไฟล์ในเครื่อง เนื้อหาสองประเภท โพสต์และผู้แต่ง จะใช้ไฟล์ประเภทต่างๆ โพสต์ที่มีข้อความจำนวนมากจะใช้ Markdown ทำให้กระบวนการแก้ไขง่ายขึ้น เนื่องจากข้อมูลเกี่ยวกับผู้เขียนนั้นเบากว่า เราจะเก็บไว้ในไฟล์ JSON ฟังก์ชัน Helper จะทำให้อ่านไฟล์ประเภทต่างๆ และรวมเนื้อหาได้ง่ายขึ้น
Next.js ช่วยให้เราอ่านข้อมูลจากแหล่งต่าง ๆ และประเภทต่าง ๆ ได้อย่างง่ายดาย ด้วยการกำหนดเส้นทางแบบไดนามิกและ next/link
เราจึงสามารถสร้างและนำทางไปยังหน้าต่างๆ ของไซต์ของเราได้อย่างรวดเร็ว เรายังได้รับการเพิ่มประสิทธิภาพรูปภาพฟรีด้วยแพ็คเกจ next/image
เมื่อเลือก "รวมแบตเตอรี่" Next.js เราสามารถมุ่งเน้นไปที่แอปพลิเคชันของเราได้ เราไม่ต้องเสียเวลากับงานพื้นฐานซ้ำๆ ที่โปรเจ็กต์ใหม่ๆ มักจะมาพร้อมกับ แทนที่จะสร้างทุกอย่างด้วยมือ เราสามารถพึ่งพากรอบงานที่ได้รับการทดสอบและพิสูจน์แล้ว ชุมชนขนาดใหญ่และแอ็คทีฟที่อยู่เบื้องหลัง Next.js ช่วยให้รับความช่วยเหลือได้ง่ายหากเราพบปัญหาระหว่างทาง
หลังจากอ่านบทความนี้ คุณจะสามารถเพิ่มเนื้อหาได้หลายประเภทในโครงการ Next.js เดียว คุณจะสามารถสร้างความสัมพันธ์ระหว่างพวกเขาได้ ที่ให้คุณเชื่อมโยงสิ่งต่างๆ เช่น ผู้แต่งและโพสต์ หลักสูตรและบทเรียน หรือนักแสดงและภาพยนตร์
บทความนี้ถือว่ามีความคุ้นเคยกับ Next.js ขั้นพื้นฐาน หากคุณไม่เคยใช้มาก่อน คุณอาจต้องการอ่านวิธีจัดการกับหน้าและดึงข้อมูลสำหรับหน้าเหล่านั้นก่อน
บทความนี้ไม่กล่าวถึงการจัดสไตล์และเน้นที่การทำให้ทุกอย่างได้ผลแทน คุณสามารถรับผลลัพธ์บน GitHub นอกจากนี้ยังมีสไตล์ชีตที่คุณสามารถวางลงในโปรเจ็กต์ของคุณได้หากต้องการติดตามพร้อมกับบทความนี้ ในการรับเฟรมเดียวกัน รวมถึงการนำทาง ให้แทนที่ไฟล์นี้ pages/_app.js
ติดตั้ง
เราเริ่มต้นด้วยการตั้งค่าโครงการใหม่โดยใช้ create-next-app
และเปลี่ยนเป็นไดเร็กทอรี:
$ npx create-next-app multiauthor-blog $ cd multiauthor-blog
เราจะต้องอ่านไฟล์ Markdown ในภายหลัง เพื่อให้ง่ายขึ้น เรายังเพิ่มการพึ่งพาอื่นๆ อีกสองสามรายการก่อนเริ่มต้น
multiauthor-blog$ yarn add gray-matter remark remark-html
เมื่อการติดตั้งเสร็จสิ้น เราสามารถเรียกใช้สคริปต์ dev
เพื่อเริ่มโครงการของเรา:
multiauthor-blog$ yarn dev
ขณะนี้เราสามารถสำรวจไซต์ของเราได้แล้ว ในเบราว์เซอร์ของคุณ ให้เปิด https://localhost:3000 คุณควรเห็นหน้าเริ่มต้นที่เพิ่มโดย create-next-app
อีกสักครู่ เราจะต้องมีการนำทางเพื่อไปยังหน้าเว็บของเรา เราสามารถเพิ่มมันใน pages/_app.js
ก่อนที่หน้าจะมีอยู่
import Link from 'next/link' import '../styles/globals.css' export default function App({ Component, pageProps }) { return ( <> <header> <nav> <ul> <li> <Link href="/"> <a>Home</a> </Link> </li> <li> <Link href="/posts"> <a>Posts</a> </Link> </li> <li> <Link href="/authors"> <a>Authors</a> </Link> </li> </ul> </nav> </header> <main> <Component {...pageProps} /> </main> </> ) }
ในบทความนี้ เราจะเพิ่มหน้าที่หายไปเหล่านี้ซึ่งชี้นำทางไป มาเพิ่มบทความกันก่อน เพื่อให้เรามีบางอย่างที่ต้องดำเนินการในหน้าภาพรวมของบล็อก
กำลังสร้างกระทู้
เพื่อแยกเนื้อหาของเราออกจากโค้ด เราจะใส่บทความในไดเร็กทอรีชื่อ _posts/
เพื่อให้การเขียนและแก้ไขง่ายขึ้น เราจะสร้างแต่ละโพสต์เป็นไฟล์ Markdown ชื่อไฟล์ของแต่ละโพสต์จะทำหน้าที่เป็นทากในเส้นทางของเราในภายหลัง ไฟล์ _posts/hello-world.md
จะสามารถเข้าถึงได้ภายใต้ /posts/hello-world
เป็นต้น
ข้อมูลบางอย่าง เช่น ชื่อเต็มและข้อความที่ตัดตอนมาสั้นๆ จะอยู่ในส่วนหน้าที่ตอนต้นของไฟล์
--- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" --- Hey, how are you doing? Welcome to my blog. In this post, …
เพิ่มไฟล์แบบนี้อีกสองสามไฟล์เพื่อไม่ให้บล็อกว่างเปล่า:
multi-author-blog/ ├─ _posts/ │ ├─ hello-world.md │ ├─ multi-author-blog-in-nextjs.md │ ├─ styling-react-with-tailwind.md │ └─ ten-hidden-gems-in-javascript.md └─ pages/ └─ …
คุณสามารถเพิ่มของคุณเองหรือคว้าโพสต์ตัวอย่างเหล่านี้จากที่เก็บ GitHub
แสดงกระทู้ทั้งหมด
ตอนนี้เรามีบทความสองสามโพสต์แล้ว เราต้องการวิธีที่จะนำโพสต์เหล่านั้นเข้าสู่บล็อกของเรา เริ่มต้นด้วยการเพิ่มหน้าที่แสดงรายการทั้งหมด ซึ่งทำหน้าที่เป็นดัชนีของบล็อกของเรา
ใน Next.js ไฟล์ที่สร้างภายใต้ pages/posts/index.js
จะสามารถเข้าถึงได้เป็น /posts
บนเว็บไซต์ของเรา ไฟล์ต้องส่งออกฟังก์ชันที่จะทำหน้าที่เป็นเนื้อหาของหน้านั้น รุ่นแรกมีลักษณะดังนี้:
export default function Posts() { return ( <div className="posts"> <h1>Posts</h1> {/* TODO: render posts */} </div> ) }
เราไปได้ไม่ไกลเพราะเรายังไม่มีวิธีอ่านไฟล์ Markdown เราสามารถนำทางไปที่ https://localhost:3000/posts ได้แล้ว แต่เราเห็นเฉพาะส่วนหัวเท่านั้น
ตอนนี้เราต้องการวิธีที่จะโพสต์ของเราที่นั่น Next.js ใช้ฟังก์ชันที่เรียกว่า getStaticProps()
เพื่อส่งข้อมูลไปยังองค์ประกอบของหน้า ฟังก์ชันส่งผ่าน props
ในวัตถุที่ส่งคืนไปยังส่วนประกอบเป็นอุปกรณ์ประกอบฉาก
จาก getStaticProps()
เราจะส่งต่อโพสต์ไปยังส่วนประกอบในรูปแบบ prop ที่เรียกว่า posts
เราจะฮาร์ดโค้ดโพสต์ตัวยึดตำแหน่งสองตัวในขั้นตอนแรกนี้ เมื่อเริ่มต้นด้วยวิธีนี้ เราจะกำหนดรูปแบบที่เราต้องการรับโพสต์จริงในภายหลัง หากฟังก์ชันตัวช่วยแสดงผลในรูปแบบนี้ เราสามารถสลับไปใช้รูปแบบดังกล่าวได้โดยไม่ต้องเปลี่ยนส่วนประกอบ
ภาพรวมของโพสต์จะไม่แสดงข้อความทั้งหมดของโพสต์ สำหรับหน้านี้ ชื่อเรื่อง ข้อความที่ตัดตอนมา ลิงก์ถาวร และวันที่ของแต่ละโพสต์ก็เพียงพอแล้ว
export default function Posts() { … } +export function getStaticProps() { + return { + props: { + posts: [ + { + title: "My first post", + createdAt: "2021-05-01", + excerpt: "A short excerpt summarizing the post.", + permalink: "/posts/my-first-post", + slug: "my-first-post", + }, { + title: "My second post", + createdAt: "2021-05-04", + excerpt: "Another summary that is short.", + permalink: "/posts/my-second-post", + slug: "my-second-post", + } + ] + } + } +}
ในการตรวจสอบการเชื่อมต่อ เราสามารถหยิบโพสต์จากอุปกรณ์ประกอบฉากและแสดงในองค์ประกอบ Posts
เราจะใส่ชื่อ วันที่สร้าง ข้อความที่ตัดตอนมา และลิงก์ไปยังโพสต์ สำหรับตอนนี้ ลิงก์นั้นจะไม่นำไปสู่ที่ใดเลย
+import Link from 'next/link' -export default function Posts() { +export default function Posts({ posts }) { return ( <div className="posts"> <h1>Posts</h1> - {/* TODO: render posts */} + {posts.map(post => { + const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { + month: 'short', + day: '2-digit', + year: 'numeric', + }) + + return ( + <article key={post.slug}> + <h2> + <Link href={post.permalink}> + <a>{post.title}</a> + </Link> + </h2> + + <time dateTime={post.createdAt}>{prettyDate}</time> + + <p>{post.excerpt}</p> + + <Link href={post.permalink}> + <a>Read more →</a> + </Link> + </article> + ) + })} </div> ) } export function getStaticProps() { … }
หลังจากโหลดหน้าซ้ำในเบราว์เซอร์ ตอนนี้จะแสดงสองโพสต์นี้:
เราไม่ต้องการฮาร์ดโค้ดโพสต์บล็อกทั้งหมดของเราใน getStaticProps()
ตลอดไป ท้ายที่สุด นั่นคือเหตุผลที่เราสร้างไฟล์เหล่านี้ทั้งหมดใน _posts/
ก่อนหน้านี้ ตอนนี้เราต้องการวิธีอ่านไฟล์เหล่านั้นและส่งเนื้อหาไปยังองค์ประกอบของหน้า
มีสองสามวิธีที่เราทำได้ เราสามารถอ่านไฟล์ได้ทันทีใน getStaticProps()
เนื่องจากฟังก์ชันนี้ทำงานบนเซิร์ฟเวอร์ไม่ใช่ไคลเอนต์ เราจึงมีสิทธิ์เข้าถึงโมดูล Node.js ดั้งเดิม เช่น fs
ในนั้น เราสามารถอ่าน แปลง และแม้กระทั่งจัดการไฟล์ในเครื่องในไฟล์เดียวกับที่เราเก็บองค์ประกอบของหน้าไว้
เพื่อให้ไฟล์สั้นและจดจ่อกับงานเดียว เราจะย้ายฟังก์ชันนั้นไปยังไฟล์อื่นแทน ด้วยวิธีนี้ คอมโพเนนต์ Posts
จะต้องแสดงข้อมูลเท่านั้น โดยไม่ต้องอ่านข้อมูลนั้นด้วย สิ่งนี้จะเพิ่มการแยกและการจัดระเบียบให้กับโครงการของเรา
ตามธรรมเนียม เราจะใส่ฟังก์ชันที่อ่านข้อมูลในไฟล์ชื่อ lib/api.js
ไฟล์นั้นจะเก็บฟังก์ชันทั้งหมดที่ดึงเนื้อหาของเราสำหรับส่วนประกอบที่แสดง
สำหรับหน้าภาพรวมโพสต์ เราต้องการฟังก์ชันที่อ่าน ประมวลผล และส่งคืนโพสต์ทั้งหมด เราจะเรียกมันว่า getAllPosts()
ในนั้น ก่อนอื่นเราใช้ path.join()
เพื่อสร้างพาธไปยัง _posts/
จากนั้นเราใช้ fs.readdirSync()
เพื่ออ่านไดเร็กทอรีนั้น ซึ่งให้ชื่อไฟล์ทั้งหมดในไดเร็กทอรีนั้นแก่เรา การทำแผนที่เหนือชื่อเหล่านี้ จากนั้นเราจะอ่านแต่ละไฟล์ตามลำดับ
import fs from 'fs' import path from 'path' export function getAllPosts() { const postsDirectory = path.join(process.cwd(), '_posts') const filenames = fs.readdirSync(postsDirectory) return filenames.map(filename => { const file = fs.readFileSync(path.join(process.cwd(), '_posts', filename), 'utf8') // TODO: transform and return file }) }
หลังจากอ่านไฟล์แล้ว เราจะได้เนื้อหาเป็นสตริงยาว ในการแยก frontmatter ออกจากข้อความในโพสต์ เรารันสตริงนั้นโดย gray-matter
นอกจากนี้เรายังจะคว้าตัวทากของแต่ละโพสต์ด้วยการลบ . .md
ออกจากท้ายชื่อไฟล์ เราต้องใช้ทากนั้นเพื่อสร้าง URL ที่จะเข้าถึงโพสต์ได้ในภายหลัง เนื่องจากเราไม่ต้องการเนื้อหา Markdown ของโพสต์สำหรับฟังก์ชันนี้ เราจึงสามารถละเว้นเนื้อหาที่เหลือได้
import fs from 'fs' import path from 'path' +import matter from 'gray-matter' export function getAllPosts() { const postsDirectory = path.join(process.cwd(), '_posts') const filenames = fs.readdirSync(postsDirectory) return filenames.map(filename => { const file = fs.readFileSync(path.join(process.cwd(), '_posts', filename), 'utf8') - // TODO: transform and return file + // get frontmatter + const { data } = matter(file) + + // get slug from filename + const slug = filename.replace(/\.md$/, '') + + // return combined frontmatter and slug; build permalink + return { + ...data, + slug, + permalink: `/posts/${slug}`, + } }) }
สังเกตว่าเรากระจาย ...data
ไปยังวัตถุที่ส่งคืนที่นี่อย่างไร ที่ช่วยให้เราเข้าถึงค่าจาก frontmatter เป็น {post.title}
แทนที่จะเป็น {post.data.title}
ในภายหลัง
กลับมาที่หน้าภาพรวมของโพสต์ ขณะนี้เราสามารถแทนที่โพสต์ตัวยึดตำแหน่งด้วยฟังก์ชันใหม่นี้ได้
+import { getAllPosts } from '../../lib/api' export default function Posts({ posts }) { … } export function getStaticProps() { return { props: { - posts: [ - { - title: "My first post", - createdAt: "2021-05-01", - excerpt: "A short excerpt summarizing the post.", - permalink: "/posts/my-first-post", - slug: "my-first-post", - }, { - title: "My second post", - createdAt: "2021-05-04", - excerpt: "Another summary that is short.", - permalink: "/posts/my-second-post", - slug: "my-second-post", - } - ] + posts: getAllPosts(), } } }
หลังจากโหลดเบราว์เซอร์ใหม่ ตอนนี้เราเห็นโพสต์จริงของเราแทนที่จะเป็นตัวยึดตำแหน่งที่เคยมี
การเพิ่มหน้าโพสต์ส่วนบุคคล
ลิงก์ที่เราเพิ่มในแต่ละโพสต์ยังไม่นำไปสู่ที่ใด ยังไม่มีหน้าที่ตอบสนองต่อ URL เช่น /posts/hello-world
ด้วยการกำหนดเส้นทางแบบไดนามิก เราสามารถเพิ่มหน้าที่ตรงกับเส้นทางทั้งหมดเช่นนี้
ไฟล์ที่สร้างเป็น pages/posts/[slug].js
จะจับคู่ URL ทั้งหมดที่ดูเหมือน /posts/abc
ค่าที่ปรากฏแทน [slug]
ใน URL จะพร้อมใช้งานในหน้าเป็นพารามิเตอร์การค้นหา เราสามารถใช้สิ่งนั้นใน getStaticProps()
ของหน้าที่เกี่ยวข้องเป็น params.slug
เพื่อเรียกใช้ฟังก์ชันตัวช่วย
ในฐานะคู่ขนานกับ getAllPosts()
เราจะเรียกฟังก์ชันตัวช่วยนั้น getPostBySlug(slug)
แทนที่จะโพสต์ทั้งหมด มันจะส่งคืนโพสต์เดียวที่ตรงกับทากที่เราส่งไป ในหน้าของโพสต์ เรายังต้องแสดงเนื้อหา Markdown ของไฟล์ที่อยู่ภายใต้
หน้าสำหรับโพสต์แต่ละรายการจะดูเหมือนหน้าภาพรวมของโพสต์ แทนที่จะส่ง posts
ไปยังเพจใน getStaticProps()
เราส่งเพียง post
เดียว มาทำการตั้งค่าทั่วไปกันก่อนที่เราจะดูวิธีแปลงเนื้อหา Markdown ของโพสต์เป็น HTML ที่ใช้งานได้ เราจะข้ามโพสต์ตัวยึดตำแหน่งที่นี่ โดยใช้ฟังก์ชันตัวช่วย เราจะเพิ่มในขั้นตอนถัดไปทันที
import { getPostBySlug } from '../../lib/api' export default function Post({ post }) { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <div className="post"> <h1>{post.title}</h1> <time dateTime={post.createdAt}>{prettyDate}</time> {/* TODO: render body */} </div> ) } export function getStaticProps({ params }) { return { props: { post: getPostBySlug(params.slug), }, } }
ตอนนี้เราต้องเพิ่มฟังก์ชัน getPostBySlug(slug)
ให้กับไฟล์ตัวช่วยของเรา lib/api.js
มันเหมือนกับ getAllPosts()
โดยมีความแตกต่างที่น่าสังเกตเล็กน้อย เนื่องจากเราสามารถรับชื่อไฟล์ของโพสต์จากกระสุน เราจึงไม่จำเป็นต้องอ่านไดเรกทอรีทั้งหมดก่อน หากกระสุนเป็น 'hello-world'
เราจะอ่านไฟล์ชื่อ _posts/hello-world.md
หากไม่มีไฟล์นั้น Next.js จะแสดงหน้าข้อผิดพลาด 404
ความแตกต่างอีกอย่างของ getAllPosts()
ก็คือ คราวนี้ เราต้องอ่านเนื้อหา Markdown ของโพสต์ด้วย เราสามารถส่งคืนเป็น HTML ที่พร้อมแสดงผลแทน Raw Markdown โดยประมวลผลด้วย remark
ก่อน
import fs from 'fs' import path from 'path' import matter from 'gray-matter' +import remark from 'remark' +import html from 'remark-html' export function getAllPosts() { … } +export function getPostBySlug(slug) { + const file = fs.readFileSync(path.join(process.cwd(), '_posts', `${slug}.md`), 'utf8') + + const { + content, + data, + } = matter(file) + + const body = remark().use(html).processSync(content).toString() + + return { + ...data, + body, + } +}
ในทางทฤษฎี เราสามารถใช้ฟังก์ชัน getAllPosts()
ภายใน getPostBySlug(slug)
อันดับแรก เราจะได้โพสต์ทั้งหมดที่มี ซึ่งเราสามารถค้นหาโพสต์ที่ตรงกับตัวทากที่ให้มา นั่นหมายความว่าเราจำเป็นต้องอ่านโพสต์ทั้งหมดก่อนจึงจะได้โพสต์เดียว ซึ่งเป็นงานที่ไม่จำเป็น getAllPosts()
จะไม่ส่งคืนเนื้อหา Markdown ของโพสต์เช่นกัน เราสามารถอัปเดตให้ทำเช่นนั้นได้ ซึ่งในกรณีนี้จะทำงานได้มากกว่าที่จำเป็นในปัจจุบัน
เนื่องจากฟังก์ชันตัวช่วยทั้งสองทำหน้าที่ต่างกัน เราจะแยกให้ออกจากกัน ด้วยวิธีนี้ เราจึงสามารถมุ่งเน้นที่ฟังก์ชันต่างๆ ได้อย่างแม่นยำและเฉพาะงานที่เราต้องการให้แต่ละฟังก์ชันทำเท่านั้น
เพจที่ใช้การกำหนดเส้นทางแบบไดนามิกสามารถจัดเตรียม getStaticPaths()
ถัดจาก getStaticProps()
ฟังก์ชันนี้บอก Next.js ว่าค่าใดของเซ็กเมนต์พาธไดนามิกที่จะสร้างเพจ เราสามารถจัดเตรียมสิ่งเหล่านั้นได้โดยใช้ getAllPosts()
และส่งคืนรายการวัตถุที่กำหนด slug
ของแต่ละโพสต์
-import { getPostBySlug } from '../../lib/api' +import { getAllPosts, getPostBySlug } from '../../lib/api' export default function Post({ post }) { … } export function getStaticProps({ params }) { … } +export function getStaticPaths() { + return { + fallback: false, + paths: getAllPosts().map(post => ({ + params: { + slug: post.slug, + }, + })), + } +}
เนื่องจากเราแยกวิเคราะห์เนื้อหา Markdown ใน getPostBySlug(slug)
เราจึงสามารถแสดงผลบนหน้าได้ทันที เราจำเป็นต้องใช้ dangerouslySetInnerHTML
SetInnerHTML สำหรับขั้นตอนนี้ เพื่อให้ Next.js สามารถแสดง HTML หลัง post.body
แม้จะมีชื่อ แต่ก็ปลอดภัยที่จะใช้พร็อพเพอร์ตี้ในสถานการณ์นี้ เนื่องจากเรามีสิทธิ์ควบคุมโพสต์ของเราอย่างเต็มที่ จึงไม่น่าเป็นไปได้ที่พวกเขาจะแทรกสคริปต์ที่ไม่ปลอดภัย
import { getAllPosts, getPostBySlug } from '../../lib/api' export default function Post({ post }) { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <div className="post"> <h1>{post.title}</h1> <time dateTime={post.createdAt}>{prettyDate}</time> - {/* TODO: render body */} + <div dangerouslySetInnerHTML={{ __html: post.body }} /> </div> ) } export function getStaticProps({ params }) { … } export function getStaticPaths() { … }
หากเราติดตามลิงก์ใดลิงก์หนึ่งจากภาพรวมของโพสต์ เราจะไปที่หน้าของโพสต์นั้นเอง
การเพิ่มผู้เขียน
ตอนนี้เราได้โพสต์ที่เชื่อมต่อกันแล้ว เราต้องทำซ้ำขั้นตอนเดียวกันสำหรับผู้แต่งของเรา ครั้งนี้ เราจะใช้ JSON แทน Markdown เพื่ออธิบาย เราสามารถผสมไฟล์ประเภทต่างๆ ในโครงการเดียวกันแบบนี้ได้ทุกเมื่อที่สมเหตุสมผล ฟังก์ชันตัวช่วยที่เราใช้ในการอ่านไฟล์จะช่วยดูแลความแตกต่างต่างๆ ให้กับเรา เพจสามารถใช้ฟังก์ชันเหล่านี้ได้โดยไม่ต้องรู้ว่าเราจัดเก็บเนื้อหาในรูปแบบใด
ขั้นแรก ให้สร้างไดเร็กทอรีชื่อ _authors/
และเพิ่มไฟล์ผู้แต่งสองสามไฟล์ลงในไดเร็กทอรี เช่นเดียวกับที่เราทำกับโพสต์ ให้ตั้งชื่อไฟล์ตามทากของผู้แต่งแต่ละคน เราจะใช้สิ่งนั้นเพื่อค้นหาผู้เขียนในภายหลัง ในแต่ละไฟล์ เราระบุชื่อเต็มของผู้เขียนในวัตถุ JSON
{ "name": "Adrian Webber" }
สำหรับตอนนี้ การมีผู้เขียนสองคนในโครงการของเราก็เพียงพอแล้ว
เพื่อให้พวกเขามีบุคลิกมากขึ้น ให้เพิ่มรูปโปรไฟล์สำหรับผู้แต่งแต่ละคนด้วย เราจะนำไฟล์สแตติกเหล่านั้นไปไว้ในไดเรกทอรี public/
โดยการตั้งชื่อไฟล์ด้วยกระสุนเดียวกัน เราสามารถเชื่อมต่อไฟล์เหล่านั้นโดยใช้แบบแผนโดยนัยเพียงอย่างเดียว เราสามารถเพิ่มเส้นทางของรูปภาพไปยังไฟล์ JSON ของผู้เขียนแต่ละคนเพื่อเชื่อมโยงทั้งสองได้ การตั้งชื่อไฟล์ทั้งหมดโดยทากทำให้เราสามารถจัดการการเชื่อมต่อนี้ได้โดยไม่ต้องเขียนมันออกมา ออบเจ็กต์ JSON จำเป็นต้องเก็บข้อมูลที่เราไม่สามารถสร้างด้วยโค้ดได้
เมื่อคุณทำเสร็จแล้ว ไดเร็กทอรีโครงการของคุณควรมีลักษณะดังนี้
multi-author-blog/ ├─ _authors/ │ ├─ adrian-webber.json │ └─ megan-carter.json ├─ _posts/ │ └─ … ├─ pages/ │ └─ … └─ public/ ├─ adrian-webber.jpg └─ megan-carter.jpg
เช่นเดียวกับโพสต์ ตอนนี้เราต้องการฟังก์ชันตัวช่วยเพื่ออ่านผู้เขียนทั้งหมดและรับผู้เขียนเป็นรายบุคคล ฟังก์ชันใหม่ getAllAuthors()
และ getAuthorBySlug(slug)
ยังอยู่ใน lib/api.js
พวกเขาทำเกือบเหมือนกันทุกประการกับคู่โพสต์ของพวกเขา เนื่องจากเราใช้ JSON เพื่ออธิบายผู้เขียน เราจึงไม่จำเป็นต้องแยกวิเคราะห์ Markdown ด้วย remark
ที่นี่ เราไม่ต้องการ gray-matter
เพื่อแยกวิเคราะห์ส่วนหน้า แต่เราสามารถใช้ JSON.parse()
ในตัวของ JavaScript เพื่ออ่านเนื้อหาข้อความของไฟล์ของเราลงในวัตถุ
const contents = fs.readFileSync(somePath, 'utf8') // ⇒ looks like an object, but is a string // eg '{ "name": "John Doe" }' const json = JSON.parse(contents) // ⇒ a real JavaScript object we can do things with // eg { name: "John Doe" }
ด้วยความรู้นั้น ฟังก์ชันตัวช่วยของเรามีลักษณะดังนี้:
export function getAllPosts() { … } export function getPostBySlug(slug) { … } +export function getAllAuthors() { + const authorsDirectory = path.join(process.cwd(), '_authors') + const filenames = fs.readdirSync(authorsDirectory) + + return filenames.map(filename => { + const file = fs.readFileSync(path.join(process.cwd(), '_authors', filename), 'utf8') + + // get data + const data = JSON.parse(file) + + // get slug from filename + const slug = filename.replace(/\.json/, '') + + // return combined frontmatter and slug; build permalink + return { + ...data, + slug, + permalink: `/authors/${slug}`, + profilePictureUrl: `${slug}.jpg`, + } + }) +} + +export function getAuthorBySlug(slug) { + const file = fs.readFileSync(path.join(process.cwd(), '_authors', `${slug}.json`), 'utf8') + + const data = JSON.parse(file) + + return { + ...data, + permalink: `/authors/${slug}`, + profilePictureUrl: `/${slug}.jpg`, + slug, + } +}
ด้วยวิธีการอ่านผู้เขียนในแอปพลิเคชันของเรา ตอนนี้เราสามารถเพิ่มหน้าที่แสดงรายการทั้งหมดได้ การสร้างหน้าใหม่ภายใต้ pages/authors/index.js
ทำให้เรามีหน้า /authors
บนเว็บไซต์ของเรา
ฟังก์ชันตัวช่วยดูแลการอ่านไฟล์ให้เรา องค์ประกอบของหน้านี้ไม่จำเป็นต้องรู้ว่าผู้เขียนเป็นไฟล์ JSON ในระบบไฟล์ มันสามารถใช้ getAllAuthors()
โดยไม่รู้ว่ามันรับข้อมูลมาจากไหนหรืออย่างไร รูปแบบไม่สำคัญตราบใดที่ฟังก์ชันตัวช่วยของเราส่งคืนข้อมูลในรูปแบบที่เราสามารถใช้ได้ นามธรรมเช่นนี้ทำให้เราผสมผสานเนื้อหาประเภทต่างๆ ในแอปของเราได้
หน้าดัชนีสำหรับผู้เขียนดูเหมือนหน้าโพสต์มาก เราได้รับผู้เขียนทั้งหมดใน getStaticProps()
ซึ่งส่งผ่านไปยังองค์ประกอบ Authors
องค์ประกอบนั้นจับคู่กับผู้แต่งแต่ละคนและแสดงรายการข้อมูลบางอย่างเกี่ยวกับพวกเขา เราไม่จำเป็นต้องสร้างลิงก์หรือ URL อื่นๆ จากกระสุน ฟังก์ชันตัวช่วยส่งคืนผู้เขียนในรูปแบบที่ใช้งานได้แล้ว
import Image from 'next/image' import Link from 'next/link' import { getAllAuthors } from '../../lib/api/authors' export default function Authors({ authors }) { return ( <div className="authors"> <h1>Authors</h1> {authors.map(author => ( <div key={author.slug}> <h2> <Link href={author.permalink}> <a>{author.name}</a> </Link> </h2> <Image alt={author.name} src={author.profilePictureUrl} height="40" width="40" /> <Link href={author.permalink}> <a>Go to profile →</a> </Link> </div> ))} </div> ) } export function getStaticProps() { return { props: { authors: getAllAuthors(), }, } }
ถ้าเราเข้าไปที่ /authors
บนเว็บไซต์ของเรา เราจะเห็นรายชื่อผู้เขียนทั้งหมดพร้อมชื่อและรูปภาพของพวกเขา
ลิงก์ไปยังโปรไฟล์ของผู้เขียนยังไม่นำไปสู่ที่ใด ในการเพิ่มหน้าโปรไฟล์ เราสร้างไฟล์ภายใต้ pages/authors/[slug].js
เนื่องจากผู้เขียนไม่มีเนื้อหาที่เป็นข้อความ ทั้งหมดที่เราเพิ่มได้ในตอนนี้คือชื่อและรูปโปรไฟล์ นอกจากนี้เรายังต้องการ getStaticPaths()
อีกอันเพื่อบอก Next.js ว่าทากใดที่จะสร้างเพจ
import Image from 'next/image' import { getAllAuthors, getAuthorBySlug } from '../../lib/api' export default function Author({ author }) { return ( <div className="author"> <h1>{author.name}</h1> <Image alt={author.name} src={author.profilePictureUrl} height="80" width="80" /> </div> ) } export function getStaticProps({ params }) { return { props: { author: getAuthorBySlug(params.slug), }, } } export function getStaticPaths() { return { fallback: false, paths: getAllAuthors().map(author => ({ params: { slug: author.slug, }, })), } }
ด้วยสิ่งนี้ ตอนนี้เรามีหน้าโปรไฟล์ผู้แต่งพื้นฐานที่ให้ข้อมูลค่อนข้างน้อย
ณ จุดนี้ ผู้เขียนและโพสต์ยังไม่ได้เชื่อมต่อ เราจะสร้างสะพานเชื่อมนั้นต่อไป เพื่อให้เราสามารถเพิ่มรายการโพสต์ของผู้เขียนแต่ละคนไปยังหน้าโปรไฟล์ของพวกเขา
การเชื่อมต่อโพสต์และผู้แต่ง
ในการเชื่อมต่อเนื้อหาสองส่วน เราต้องอ้างอิงเนื้อหาหนึ่งไปอีกส่วนหนึ่ง เนื่องจากเราระบุโพสต์และผู้แต่งด้วยทากของโพสต์แล้ว เราจะอ้างอิงถึงสิ่งนั้นด้วย เราสามารถเพิ่มผู้เขียนบทความและบทความให้กับผู้เขียนได้ แต่ทิศทางเดียวก็เพียงพอแล้วที่จะเชื่อมโยงพวกเขา เนื่องจากเราต้องการระบุแหล่งที่มาของโพสต์ให้กับผู้เขียน เราจะเพิ่มทากของผู้เขียนลงในส่วนหน้าของโพสต์แต่ละโพสต์
--- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" +author: adrian-webber --- Hey, how are you doing? Welcome to my blog. In this post, …
หากเราเก็บไว้อย่างนั้น การเรียกใช้โพสต์ผ่าน gray-matter
จะเพิ่มฟิลด์ผู้เขียนลงในโพสต์เป็นสตริง:
const post = getPostBySlug("hello-world") const author = post.author console.log(author) // "adrian-webber"
เพื่อให้ได้วัตถุที่เป็นตัวแทนของผู้เขียน เราสามารถใช้กระสุนนั้นแล้วเรียก getAuthorBySlug(slug)
กับมัน
const post = getPostBySlug("hello-world") -const author = post.author +const author = getAuthorBySlug(post.author) console.log(author) // { // name: "Adrian Webber", // slug: "adrian-webber", // profilePictureUrl: "/adrian-webber.jpg", // permalink: "/authors/adrian-webber" // }
ในการเพิ่มผู้เขียนในหน้าของโพสต์เดียว เราต้องเรียก getAuthorBySlug(slug)
หนึ่งครั้งใน getStaticProps()
+import Image from 'next/image' +import Link from 'next/link' -import { getPostBySlug } from '../../lib/api' +import { getAuthorBySlug, getPostBySlug } from '../../lib/api' export default function Post({ post }) { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <div className="post"> <h1>{post.title}</h1> <time dateTime={post.createdAt}>{prettyDate}</time> + <div> + <Image alt={post.author.name} src={post.author.profilePictureUrl} height="40" width="40" /> + + <Link href={post.author.permalink}> + <a> + {post.author.name} + </a> + </Link> + </div> <div dangerouslySetInnerHTML={{ __html: post.body }}> </div> ) } export function getStaticProps({ params }) { + const post = getPostBySlug(params.slug) return { props: { - post: getPostBySlug(params.slug), + post: { + ...post, + author: getAuthorBySlug(post.author), + }, }, } }
สังเกตว่าเรากระจาย ...post
ไปยังวัตถุที่เรียกว่า post
ใน getStaticProps()
อย่างไร โดยการวาง author
ไว้หลังบรรทัดนั้น เราจะแทนที่เวอร์ชันสตริงของผู้แต่งด้วยอ็อบเจกต์แบบเต็ม ที่ช่วยให้เราเข้าถึงคุณสมบัติของผู้เขียนผ่าน post.author.name
ในองค์ประกอบ Post
ด้วยการเปลี่ยนแปลงดังกล่าว ตอนนี้เราได้รับลิงก์ไปยังหน้าโปรไฟล์ของผู้เขียน พร้อมด้วยชื่อและรูปภาพ ในหน้าของโพสต์
การเพิ่มผู้เขียนในหน้าภาพรวมของโพสต์จำเป็นต้องมีการเปลี่ยนแปลงที่คล้ายกัน แทนที่จะเรียก getAuthorBySlug(slug)
ครั้งเดียว เราต้องแมปโพสต์ทั้งหมดและเรียกมันสำหรับแต่ละโพสต์
+import Image from 'next/image' +import Link from 'next/link' -import { getAllPosts } from '../../lib/api' +import { getAllPosts, getAuthorBySlug } from '../../lib/api' export default function Posts({ posts }) { return ( <div className="posts"> <h1>Posts</h1> {posts.map(post => { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <article key={post.slug}> <h2> <Link href={post.permalink}> <a>{post.title}</a> </Link> </h2> <time dateTime={post.createdAt}>{prettyDate}</time> + <div> + <Image alt={post.author.name} src={post.author.profilePictureUrl} height="40" width="40" /> + + <span>{post.author.name}</span> + </div> <p>{post.excerpt}</p> <Link href={post.permalink}> <a>Read more →</a> </Link> </article> ) })} </div> ) } export function getStaticProps() { return { props: { - posts: getAllPosts(), + posts: getAllPosts().map(post => ({ + ...post, + author: getAuthorBySlug(post.author), + })), } } }
ที่เพิ่มผู้เขียนในแต่ละโพสต์ในภาพรวมโพสต์:
เราไม่จำเป็นต้องเพิ่มรายการโพสต์ของผู้เขียนลงในไฟล์ JSON ในหน้าโปรไฟล์ของพวกเขา ก่อนอื่นเราจะรับโพสต์ทั้งหมดด้วย getAllPosts()
จากนั้น เราสามารถกรองรายการทั้งหมดสำหรับรายการที่เกี่ยวข้องกับผู้เขียนรายนี้
import Image from 'next/image' +import Link from 'next/link' -import { getAllAuthors, getAuthorBySlug } from '../../lib/api' +import { getAllAuthors, getAllPosts, getAuthorBySlug } from '../../lib/api' export default function Author({ author }) { return ( <div className="author"> <h1>{author.name}</h1> <Image alt={author.name} src={author.profilePictureUrl} height="40" width="40" /> + <h2>Posts</h2> + + <ul> + {author.posts.map(post => ( + <li> + <Link href={post.permalink}> + <a> + {post.title} + </a> + </Link> + </li> + ))} + </ul> </div> ) } export function getStaticProps({ params }) { const author = getAuthorBySlug(params.slug) return { props: { - author: getAuthorBySlug(params.slug), + author: { + ...author, + posts: getAllPosts().filter(post => post.author === author.slug), + }, }, } } export function getStaticPaths() { … }
สิ่งนี้ทำให้เรามีรายชื่อบทความในหน้าโปรไฟล์ของผู้เขียนทุกคน
ในหน้าภาพรวมผู้เขียน เราจะเพิ่มเฉพาะจำนวนโพสต์ที่พวกเขาเขียนเพื่อไม่ให้อินเทอร์เฟซรก
import Image from 'next/image' import Link from 'next/link' -import { getAllAuthors } from '../../lib/api' +import { getAllAuthors, getAllPosts } from '../../lib/api' export default function Authors({ authors }) { return ( <div className="authors"> <h1>Authors</h1> {authors.map(author => ( <div key={author.slug}> <h2> <Link href={author.permalink}> <a> {author.name} </a> </Link> </h2> <Image alt={author.name} src={author.profilePictureUrl} height="40" width="40" /> + <p>{author.posts.length} post(s)</p> <Link href={author.permalink}> <a>Go to profile →</a> </Link> </div> ))} </div> ) } export function getStaticProps() { return { props: { - authors: getAllAuthors(), + authors: getAllAuthors().map(author => ({ + ...author, + posts: getAllPosts().filter(post => post.author === author.slug), + })), } } }
ด้วยเหตุนี้ หน้าภาพรวมของผู้เขียนจะแสดงจำนวนโพสต์ที่ผู้เขียนแต่ละคนมีส่วนร่วม
และนั่นแหล่ะ! โพสต์และผู้แต่งเชื่อมโยงกันอย่างสมบูรณ์แล้ว เราสามารถรับจากโพสต์ไปยังหน้าโปรไฟล์ของผู้เขียน และจากที่นั่นไปยังโพสต์อื่นๆ ของพวกเขา
สรุปและ Outlook
ในบทความนี้ เราเชื่อมโยงเนื้อหาสองประเภทที่เกี่ยวข้องผ่านทากที่มีลักษณะเฉพาะ การกำหนดความสัมพันธ์จากโพสต์ถึงผู้เขียนทำให้เกิดสถานการณ์ที่หลากหลาย ตอนนี้เราสามารถแสดงผู้เขียนในแต่ละโพสต์และแสดงโพสต์ของพวกเขาในหน้าโปรไฟล์ของพวกเขา
ด้วยเทคนิคนี้ เราสามารถเพิ่มความสัมพันธ์ประเภทอื่นๆ ได้มากมาย แต่ละโพสต์อาจมีผู้วิจารณ์อยู่เหนือผู้เขียน เราสามารถตั้งค่าได้โดยการเพิ่มฟิลด์ reviewer
ในส่วนหน้าของโพสต์
--- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" author: adrian-webber +reviewer: megan-carter --- Hey, how are you doing? Welcome to my blog. In this post, …
ในระบบไฟล์ ผู้ตรวจสอบคือผู้เขียนอีกคนหนึ่งจาก _authors/
เราสามารถใช้ getAuthorBySlug(slug)
เพื่อรับข้อมูลได้เช่นกัน
export function getStaticProps({ params }) { const post = getPostBySlug(params.slug) return { props: { post: { ...post, author: getAuthorBySlug(post.author), + reviewer: getAuthorBySlug(post.reviewer), }, }, } }
เราสามารถสนับสนุนผู้เขียนร่วมได้ด้วยการตั้งชื่อผู้เขียนสองคนขึ้นไปในโพสต์ แทนที่จะเป็นเพียงคนเดียว
--- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" -author: adrian-webber +authors: + - adrian-webber + - megan-carter --- Hey, how are you doing? Welcome to my blog. In this post, …
ในสถานการณ์นี้ เราไม่สามารถค้นหาผู้เขียนคนเดียวในโพสต์ getStaticProps()
ได้อีกต่อไป แต่เราจะทำแผนที่เหนืออาร์เรย์ของผู้เขียนเพื่อให้ได้มาทั้งหมด
export function getStaticProps({ params }) { const post = getPostBySlug(params.slug) return { props: { post: { ...post, - author: getAuthorBySlug(post.author), + authors: post.authors.map(getAuthorBySlug), }, }, } }
เรายังสามารถสร้างสถานการณ์อื่นๆ ด้วยเทคนิคนี้ มันเปิดใช้งานความสัมพันธ์แบบตัวต่อตัว ตัวต่อตัว หรือแม้แต่แบบกลุ่มต่อกลุ่มแบบใดก็ได้ หากโครงการของคุณมีจดหมายข่าวและกรณีศึกษาด้วย คุณสามารถเพิ่มผู้เขียนในแต่ละเรื่องได้เช่นกัน
บนไซต์ที่เกี่ยวกับจักรวาลของ Marvel เราสามารถเชื่อมโยงตัวละครและภาพยนตร์ที่พวกเขาปรากฏตัวได้ ในกีฬา เราสามารถเชื่อมต่อผู้เล่นและทีมที่พวกเขาเล่นด้วยในปัจจุบัน
เนื่องจากฟังก์ชันตัวช่วยซ่อนแหล่งข้อมูล เนื้อหาอาจมาจากระบบต่างๆ เราสามารถอ่านบทความจากระบบไฟล์ ความคิดเห็นจาก API และรวมไว้ในโค้ดของเรา หากเนื้อหาบางส่วนเกี่ยวข้องกับเนื้อหาประเภทอื่น เราสามารถเชื่อมต่อกับรูปแบบนี้ได้
แหล่งข้อมูลเพิ่มเติม
Next.js นำเสนอพื้นหลังเพิ่มเติมเกี่ยวกับฟังก์ชันที่เราใช้ในหน้าการดึงข้อมูล ประกอบด้วยลิงก์ไปยังโครงการตัวอย่างที่ดึงข้อมูลจากแหล่งที่มาประเภทต่างๆ
หากคุณต้องการดำเนินโครงการเริ่มต้นนี้ต่อไป โปรดอ่านบทความเหล่านี้:
- การสร้างโคลนเว็บไซต์ CSS Tricks ด้วย Strapi และ Next.js
แทนที่ไฟล์บนระบบไฟล์ในเครื่องด้วยแบ็กเอนด์ที่ขับเคลื่อนด้วย Strapi - การเปรียบเทียบวิธีการจัดแต่งทรงผมใน Next.js
สำรวจวิธีการต่างๆ ในการเขียน CSS ที่กำหนดเองเพื่อเปลี่ยนสไตล์ของผู้เริ่มต้น - Markdown/MDX ด้วย Next.js
เพิ่ม MDX ให้กับโปรเจ็กต์ของคุณ เพื่อให้คุณสามารถใช้ส่วนประกอบ JSX และ React ใน Markdown ของคุณได้