إنشاء مدونة متعددة المؤلفين باستخدام Next.js

نشرت: 2022-03-10
ملخص سريع ↬ توضح هذه المقالة كيف يمكننا توصيل أنواع مختلفة من المحتوى في تطبيق Next.js. باستخدام هذه التقنية ، يمكننا إضافة أي نوع من علاقة رأس برأس أو علاقة رأس بأطراف أو حتى علاقة أطراف بأطراف لمشروعاتنا.

في هذه المقالة ، سننشئ مدونة باستخدام Next.js تدعم مؤلفين أو أكثر. سننسب كل منشور إلى مؤلف وسنظهر اسمه وصورته مع مشاركاته. يحصل كل مؤلف أيضًا على صفحة ملف شخصي ، والتي تسرد جميع المشاركات التي ساهموا بها. سيبدو شيئا من هذا القبيل:

على اليسار: فهرس المدونة النهائي الذي سننشئه. على اليسار: صفحة المشاركة الفردية ، والتي ترتبط بصفحة الملف الشخصي لمؤلفها.
على اليسار: فهرس المدونة النهائي الذي سنقوم ببنائه. على اليمين: صفحة المنشور الفردي ، والتي ترتبط بصفحة الملف الشخصي لمؤلفها. (معاينة كبيرة)
صفحة الملف الشخصي للمؤلف ، مع ربط جميع مشاركاته.
صفحة الملف الشخصي للمؤلف ، مع ربط جميع منشوراته (معاينة كبيرة)

سنحتفظ بجميع المعلومات في الملفات على نظام الملفات المحلي. سيستخدم نوعا المحتوى ، المنشورات والمؤلفون ، أنواعًا مختلفة من الملفات. ستستخدم المنشورات ذات النص الثقيل Markdown ، مما يسمح بعملية تحرير أسهل. لأن المعلومات عن المؤلفين أخف ، سنحتفظ بذلك في ملفات JSON. ستجعل وظائف المساعد قراءة أنواع الملفات المختلفة ودمج محتواها بشكل أسهل.

يتيح لنا Next.js قراءة البيانات من مصادر مختلفة وأنواع مختلفة دون عناء. بفضل التوجيه الديناميكي next/link ، يمكننا إنشاء صفحات موقعنا المختلفة والتنقل إليها بسرعة. نحصل أيضًا على تحسين الصورة مجانًا مع الحزمة next/image .

باختيار Next.js "البطاريات المضمنة" ، يمكننا التركيز على تطبيقنا نفسه. لا يتعين علينا قضاء أي وقت في العمل الأساسي المتكرر للمشاريع الجديدة التي غالبًا ما تأتي معها. بدلاً من بناء كل شيء يدويًا ، يمكننا الاعتماد على إطار العمل الذي تم اختباره وثبوته. يسهل المجتمع الكبير والنشط وراء Next.js الحصول على المساعدة إذا واجهتنا مشكلات على طول الطريق.

بعد قراءة هذه المقالة ، ستتمكن من إضافة العديد من أنواع المحتوى إلى مشروع Next.js واحد. ستتمكن أيضًا من إنشاء علاقات بينهم. يتيح لك ذلك ربط أشياء مثل المؤلفين والمشاركات والدورات والدروس أو الممثلين والأفلام.

تفترض هذه المقالة الإلمام الأساسي بـ Next.js. إذا لم تكن قد استخدمته من قبل ، فقد ترغب في قراءة كيفية تعامله مع الصفحات وجلب البيانات لها أولاً.

لن نغطي التصميم في هذه المقالة ونركز على جعل كل شيء يعمل بدلاً من ذلك. يمكنك الحصول على النتيجة على جيثب. هناك أيضًا ورقة أنماط يمكنك وضعها في مشروعك إذا كنت تريد المتابعة مع هذه المقالة. للحصول على نفس الإطار ، بما في ذلك التنقل ، 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.

يتم إنشاء الصفحة الافتراضية عن طريق 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 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() ، سنقوم بتمرير المنشورات إلى المكون كدعم يسمى 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 }) }

بعد قراءة الملف ، نحصل على محتوياته كسلسلة طويلة. لفصل المادة الأمامية عن نص المنشور ، نقوم بتشغيل هذه السلسلة من خلال 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 في الكائن المرتجع هنا. يتيح لنا ذلك الوصول إلى القيم من مادته الأمامية مثل {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() ، مع بعض الاختلافات الملحوظة. نظرًا لأنه يمكننا الحصول على اسم ملف المنشور من الرابط الثابت ، لا نحتاج إلى قراءة الدليل بالكامل أولاً. إذا كانت slug هي 'hello-world' ، فسنقرأ ملفًا يسمى _posts/hello-world.md . إذا لم يكن هذا الملف موجودًا ، فسيعرض Next.js صفحة خطأ 404.

هناك اختلاف آخر في getAllPosts() وهو أنه هذه المرة ، نحتاج أيضًا إلى قراءة محتوى Markdown الخاص بالمنشور. يمكننا إعادته بتنسيق HTML جاهز للعرض بدلاً من 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) ، يمكننا عرضه على الصفحة الآن. نحتاج إلى استخدام SetInnerHTML بشكل dangerouslySetInnerHTML لهذه الخطوة حتى يتمكن 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/ . من خلال تسمية الملفات باستخدام نفس slug ، يمكننا توصيلها باستخدام الاصطلاح الضمني وحده. يمكننا إضافة مسار الصورة إلى ملف JSON لكل مؤلف لربط الاثنين. من خلال تسمية جميع الملفات بواسطة slugs ، يمكننا إدارة هذا الاتصال دون الحاجة إلى كتابته. تحتاج كائنات 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 ما هي slugs لبناء الصفحات من أجلها.

 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"

للحصول على الكائن الذي يمثل المؤلف ، يمكننا استخدام هذا slug واستدعاء 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), + })), } } }

مع ذلك ، تُظهر صفحة نظرة عامة على المؤلفين عدد المشاركات التي ساهم بها كل مؤلف.

قائمة المؤلفين مع عدد مشاركاتهم.
يمكننا الآن وضع عدد مشاركاتهم المساهمة مع إدخال كل مؤلف. (معاينة كبيرة)

وهذا كل شيء! المنشورات والمؤلفون مرتبطون بالكامل الآن. يمكننا الانتقال من منشور إلى صفحة الملف الشخصي للمؤلف ، ومن هناك إلى منشوراته الأخرى.

ملخص وتوقعات

في هذه المقالة ، قمنا بربط نوعين مرتبطين من المحتوى من خلال الارتزاق الفريد الخاص بهما. أدى تحديد العلاقة من منشور لآخر إلى تمكين مجموعة متنوعة من السيناريوهات. يمكننا الآن إظهار المؤلف في كل منشور وإدراج منشوراته على صفحات ملفه الشخصي.

باستخدام هذه التقنية ، يمكننا إضافة العديد من أنواع العلاقات الأخرى. قد يكون لكل منشور مراجع فوق المؤلف. يمكننا إعداد ذلك عن طريق إضافة حقل 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 ، يمكننا ربط الشخصيات والأفلام التي تظهر فيها. في الرياضة ، يمكننا ربط اللاعبين والفرق التي يلعبون بها حاليًا.

نظرًا لأن الوظائف المساعدة تخفي مصدر البيانات ، فقد يأتي المحتوى من أنظمة مختلفة. يمكننا قراءة المقالات من نظام الملفات والتعليقات من واجهة برمجة التطبيقات ودمجها في الكود الخاص بنا. إذا كان جزء من المحتوى يتعلق بنوع آخر من المحتوى ، فيمكننا ربطه بهذا النمط.

مزيد من الموارد

يقدم Next.js مزيدًا من الخلفية حول الوظائف التي استخدمناها في صفحتهم في جلب البيانات. يتضمن روابط لمشاريع نموذجية تجلب البيانات من أنواع مختلفة من المصادر.

إذا كنت ترغب في المضي قدمًا في مشروع البداية هذا ، فراجع هذه المقالات:

  • بناء موقع ويب CSS Tricks Clone باستخدام Strapi و Next.js
    استبدل الملفات الموجودة على نظام الملفات المحلي بخلفية تدعمها Strai.
  • مقارنة أساليب التصميم في Next.js
    اكتشف طرقًا مختلفة لكتابة CSS مخصصة لتغيير تصميم هذا المبدئ.
  • Markdown / MDX مع Next.js
    أضف MDX إلى مشروعك حتى تتمكن من استخدام مكونات JSX و React في Markdown الخاص بك.