Начальная настройка
Это серия статей в блоге, в которых подробно описывается, как я настраиваю свой сайт с помощью стека t3. Если вы не читали предыдущие статьи, рекомендую посмотреть
- Часть 1 : Редизайн моего сайта с помощью t3-stack
- Часть 2 : Отображение отдельных статей
- Часть 3 : Добавление оглавления к статьям нашего блога
- Часть 4 : Приведение в порядок конечного продукта
Основные цели в этой статье
Вот некоторые из наших основных целей, над которыми мы будем работать в этой статье
- Переписать существующие slug’ы для страниц статей так, чтобы они отображались от
<url>/blog/1
, что, честно говоря, довольно некрасиво, до<url>/blog/<slug, сгенерированного из title>
. - Рефакторинг главной страницы для использования SSG вместо tRPC useQuery hook, что может привести к тому, что в какой-то момент мне ограничат тариф.
- Украшение главной страницы
Переписывание существующих слизней
Предыдущая конфигурация
Во второй части этой серии мы рассмотрели использование Github Issues в качестве CMS. Одна из вещей, которую мы там реализовали, заключалась в получении данных из Github Issues с помощью номера выпуска, присвоенного статье.
Это приводило к таким url-слогам, как <url>/blog/1
, на которые, признаться, было не очень приятно смотреть. Я надеялся, что урлы статей будут выглядеть примерно так <url>/blog/<slug generated from title>
.
Если вы не знаете, что такое slug, это модифицированная версия строки, которая может быть использована в url. Хорошим примером может быть статья с заголовком «Добавление оглавления в статьи блога», которая может быть преобразована в slug
adding-a-table-of-contents-to-blog-articles
. Обратите внимание, что все буквы здесь строчные, а пробелы заменены на-
.
Давайте начнем с написания быстрой функции для генерации слогов для наших отдельных заголовков. Мы можем сделать это с помощью цепочки вызовов функции replace
.
export const slugify = (text: string) => {
return text
.toString()
.toLowerCase()
.replace(/s+/g, "-") // Replace spaces with -
.replace(/[^w-]+/g, "") // Remove all non-word chars
.replace(/--+/g, "-") // Replace multiple - with single -
.replace(/^-+/, "") // Trim - from start of text
.replace(/-+$/, ""); // Trim - from end of text
};
Это позволит нам теперь генерировать наши слоганы. Теперь нам нужно переписать наш файл [issueId].tsx
, который будет статически генерировать все соответствующие страницы для каждой соответствующей статьи.
Рефакторинг getStaticProps
Давайте сначала переименуем [issueId].tsx
в [slug].tsx
, чтобы он более точно отражал то, чего мы пытаемся достичь. У нас есть две функции, которые нужно изменить/записать:
getPostIDs
.
В настоящее время мы возвращаем идентификаторы отдельных выпусков различных выпусков, которые есть в нашем репозитории, используя функцию, как показано ниже.
export const getPostIds: () => Promise<number[]> = async () => {
const { repository } = await graphqlWithAuth(
`query getPostIds {
repository(owner: "ivanleomk", name: "personal_website_v2") {
issues(last: 100) {
nodes {
number
}
}
}
}
`,
{}
);
// Quick Type Definition here
return repository.issues.nodes.map((issue: { number: number }) => {
return issue.number;
});
};
Нам нужны названия отдельных статей, поэтому мы должны просто заменить number
на title
. Это даст нам новую функцию, которая выглядит примерно так, как показано ниже
export const getPostIds: () => Promise<string[]> = async () => {
const { repository } = await graphqlWithAuth(
`query getPostIds {
repository(owner: "ivanleomk", name: "personal_website_v2") {
issues(last: 100) {
nodes {
title
}
}
}
}
`
);
// Quick Type Definition here
return repository.issues.nodes.map((issue: { title: string }) => issue.title);
};
В свою очередь мы видим, что эта функция возвращает нам список отдельных названий. Затем мы можем вызвать его в getStaticPaths
, чтобы сгенерировать соответствующие пути для нашей конкретной функции, как показано ниже.
Если вы не знаете, что делает getStaticPaths, он генерирует список путей, которые могут быть сопоставлены с помощью шаблона
[slug]
. Таким образом, мы можем предотвратить доступ пользователей к несанкционированным/неправильным путям, и эта функция встроена в NextJS.
export async function getStaticPaths() {
const posts = await getPostIds();
const paths = posts.map((issueId) => `/blog/${slugify(issueId)}`);
return {
paths,
fallback: false,
};
}
getSinglePost
Я долго мучился, пытаясь найти оптимальное решение, и понял, что действительно имею дело с проблемой Yacht. Подробнее об этом вы можете прочитать здесь. В результате я понял, что мне следует отдать предпочтение созданию надежного прототипа и вместо этого работать над его оптимизацией.
Подумав немного, я понял, что потенциальным решением может быть следующее
- Получить релевантные данные о каждом посте в виде огромного запроса
- Отфильтровать все эти данные и найти пост с заголовком, который соответствует slug
- Возвращать данные, которые были отфильтрованы.
Я думаю, что основной недостаток такого подхода заключается в том, что я делаю несколько повторяющихся вызовов одного и того же API для получения одной и той же полезной нагрузки, но я не был уверен, как лучше всего запомнить данные при генерации данных с помощью NextjS. Сначала мы начнем с быстрого запроса graphql, который даст нам все необходимые данные, как показано ниже.
query getPost{
repository(owner: "ivanleomk", name: "personal_website_v2") {
issues(last:100){
edges{
node{
title
number
createdAt
body
}
}
}
}
Затем мы отфильтруем весь этот список информации с помощью простой функции высшего порядка filter
.
const post = repository.issues.edges.filter((issue) => {
return slugify(issue.node.title) === slug;
})[0].node;
прежде чем, наконец, вернуть одноэлементный узел, который соответствует действительной статье. Мы можем объединить это в виде функции getSinglePost
, как показано ниже
export const getSinglePost: (slug: string) => Promise<githubPost> = async (
slug: string
) => {
const { repository } = await graphqlWithAuth(
`
query getPost{
repository(owner: "ivanleomk", name: "personal_website_v2") {
issues(last:100){
edges{
node{
title
number
createdAt
body
}
}
}
}
}
`
);
const post = repository.issues.edges.filter((issue) => {
return slugify(issue.node.title) === slug;
})[0].node;
return post;
};
Затем мы можем вызвать это в нашей функции getStaticProps
. Это заменит нашу начальную функцию getPostByIssueId
, которую мы написали в части 2.
Как только это будет сделано, мы сможем вернуть это в нашей функции getStaticProps
, как показано ниже.
export async function getStaticProps({ params }: BlogPostParams) {
const { slug } = params;
// const post = await getPostByIssueId(parseInt(issueId));
const post = await getSinglePost(slugify(slug));
const { title, body, createdAt } = post;
const { content: parsedBody } = matter(body);
const content = await renderToHTML(parsedBody);
return {
props: {
content: String(content),
title,
createdAt,
rawContent: body,
},
};
В остальном код внутри идентичен. Затем мы можем сохранить наши изменения, обновить страницу и вуаля, мы перевели наши статьи на новый стандарт url. Нам просто нужно обновить ссылки, которые мы вводим в PostLink
, чтобы они использовали новый стандарт url, и мы перевели наши страницы статей на новый стандарт URL.
{posts.data?.posts?.map((item) => {
return <PostLink key={item.title} post={item} />;
})}
Рефакторинг index.tsx
для использования SSG вместо useQuery
Почему нам нужно изменить этот .tsx параметр
В настоящее время наш index.tsx
выглядит примерно так
import type { NextPage } from "next";
import Head from "next/head";
import PostLink from "../components/PostLink";
import { trpc } from "../utils/trpc";
const Home: NextPage = () => {
const posts = trpc.useQuery(["github.get-posts"]);
return (
<>
<h1>Posts</h1>
{posts.data?.posts?.map((item) => {
return <PostLink key={item.title} post={item} />;
})}
</>
);
};
export default Home;
он выполняет свою работу, но проблема в том, что каждый раз, когда кто-то загружает страницу, мы делаем вызов к github API. Это означает, что если 1000 человек зайдут на мой сайт в течение часа, я буду ограничен в скорости. В результате, я думаю, что это довольно вопиющая ошибка, которую нужно исправить.
Вот тут-то и приходит на помощь наш замечательный друг getStaticProps
! Обратите внимание, что здесь нам не нужно объявлять getStaticPaths
, потому что мы не используем подстановочный знак, как в [slug].tsx
. Вместо этого у нас есть только один определенный маршрут, а именно <url>/
, который мы сопоставляем в index.tsx
.
Поэтому мы можем добавить функцию getStaticProps
, которая загружает весь список постов при генерации индексной страницы. Давайте быстро разберемся с этим.
Написание функции getStaticProps
У нас уже есть написанная нами функция getPublishedPosts
, которая извлекает метаданные всех опубликованных постов. Однако, поскольку это главная страница, я хотел бы отображать только 10 последних сообщений, а не все мои сообщения. Таким образом, пользователи не будут чувствовать себя перегруженными моим сайтом и смогут увидеть более свежий и актуальный контент. Для начала мы перепишем нашу функцию getPublishedPosts
, чтобы она возвращала только 10 статей за раз.
export const getPublishedPosts: () => Promise<githubPostTitle[]> = async () => {
const { repository } = await graphqlWithAuth(`
{
repository(owner: "ivanleomk", name: "personal_website_v2") {
issues(last: 10) {
edges {
node {
number
title
createdAt
body
labels(first: 3) {
nodes{
name
}
}
}
}
}
}
}
`);
return (
repository.issues.edges
//@ts-ignore
.map(({ node }) => {
return { ...node };
})
.filter((post: githubPostTitle) => {
return post.labels.nodes.some((label) => label.name === "published");
})
);
};
Затем мы можем вызвать эту функцию в getStaticProps
нашего компонента Home
.
export async function getStaticProps() {
const posts = await getPublishedPosts();
return {
props: {
posts,
},
};
}
что позволяет нам передать список Posts
в наш компонент в качестве реквизита. Затем мы можем использовать наше предыдущее объявление githubPost
, которое мы написали в предыдущем посте, чтобы ввести этот реквизит, что позволит нам создать новую страницу Home, как показано ниже.
import Head from "next/head";
import PostLink from "../components/PostLink";
import { getPublishedPosts, githubPostTitle } from "../utils/github";
type HomePageProps = {
posts: githubPostTitle[];
};
const Home = ({ posts }: HomePageProps) => {
return (
<>
<h1>Posts</h1>
{posts &&
posts?.map((item) => {
return <PostLink key={item.title} post={item} />;
})}
</>
);
};
export async function getStaticProps() {
const posts = await getPublishedPosts();
return {
props: {
posts,
},
};
}
export default Home;
Таким образом, мы успешно перенесли нашу индексную страницу на статически генерируемую страницу с помощью SSG! Теперь осталось только подправить сгенерированные нами ссылки на посты. В идеале, мы хотим добавить больше метаданных и информации для них, чтобы пользователи могли сделать более обоснованный выбор при просмотре.
В настоящее время они выглядят следующим образом
и я думаю, что мы можем сделать гораздо лучше. Давайте теперь добавим немного парсинга, чтобы отобразить данные, которые включают в себя
- Дата создания
- Теги, которые я хочу отобразить для этой конкретной статьи.
Не стесняйтесь скопировать следующий код для достижения желаемого результата
который соответствует коду, показанному ниже
import PostLink from "../components/PostLink";
import { getPublishedPosts, githubPostTitle } from "../utils/github";
type HomePageProps = {
posts: githubPostTitle[];
};
const Home = ({ posts }: HomePageProps) => {
return (
<div className="flex items-center justify-center mt-10">
<div className="max-w-4xl w-full ">
<div>
<h2 className="text-3xl font-extrabold tracking-tight sm:text-4xl">
Yo I'm Ivan
</h2>
<p className="text-xl text-gray-500">I'm a software engineer</p>
</div>
<div className="container mx-auto mt-10">
<div className="max-w-lg">
<div className="flex flex-col items-start justify-content">
<h1 className="font-bold text-lg tracking-tight">Latest Posts</h1>
<ul>
{posts &&
posts?.map((item) => {
return <PostLink key={item.title} post={item} />;
})}
</ul>
</div>
</div>
</div>
</div>
</div>
);
};
export async function getStaticProps() {
const posts = await getPublishedPosts();
return {
props: {
posts,
},
};
}
export default Home;
И код для ссылки на пост. Обратите внимание, что вам потребуется установить day.js
для того, чтобы иметь возможность красиво оформить код.
import Link from "next/link";
import React from "react";
import { githubPostTitle } from "../utils/github";
import { slugify } from "../utils/string";
import dayjs from "dayjs";
type PostLinkProps = {
post: githubPostTitle;
};
const PostLink = ({ post }: PostLinkProps) => {
return (
<Link href={`blog/${slugify(post.title)}`}>
<div className=" py-3 pl-5">
<p className="cursor-pointer hover:underline">
{post.title} | {dayjs(post.createdAt).format("DD-MM-YYYY")}
</p>
<div className="ml-2 flex-shrink-0 flex"></div>
</div>
</Link>
);
};
export default PostLink;
Теперь осталось только автоматически сгенерировать код, который позволит нам отображать каждую отдельную серию постов, которые мы написали! Об этом мы расскажем в следующей статье, так что следите за новостями! 🙂