Приведение в порядок готового продукта


Начальная настройка

Это серия статей в блоге, в которых подробно описывается, как я настраиваю свой сайт с помощью стека t3. Если вы не читали предыдущие статьи, рекомендую посмотреть

  • Часть 1 : Редизайн моего сайта с помощью t3-stack
  • Часть 2 : Отображение отдельных статей
  • Часть 3 : Добавление оглавления к статьям нашего блога
  • Часть 4 : Приведение в порядок конечного продукта

Основные цели в этой статье

Вот некоторые из наших основных целей, над которыми мы будем работать в этой статье

  1. Переписать существующие slug’ы для страниц статей так, чтобы они отображались от <url>/blog/1, что, честно говоря, довольно некрасиво, до <url>/blog/<slug, сгенерированного из title>.
  2. Рефакторинг главной страницы для использования SSG вместо tRPC useQuery hook, что может привести к тому, что в какой-то момент мне ограничат тариф.
  3. Украшение главной страницы

Переписывание существующих слизней

Предыдущая конфигурация

Во второй части этой серии мы рассмотрели использование 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. Подробнее об этом вы можете прочитать здесь. В результате я понял, что мне следует отдать предпочтение созданию надежного прототипа и вместо этого работать над его оптимизацией.

Подумав немного, я понял, что потенциальным решением может быть следующее

  1. Получить релевантные данные о каждом посте в виде огромного запроса
  2. Отфильтровать все эти данные и найти пост с заголовком, который соответствует slug
  3. Возвращать данные, которые были отфильтрованы.

Я думаю, что основной недостаток такого подхода заключается в том, что я делаю несколько повторяющихся вызовов одного и того же 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&apos;m Ivan
          </h2>
          <p className="text-xl text-gray-500">I&apos;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;
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь осталось только автоматически сгенерировать код, который позволит нам отображать каждую отдельную серию постов, которые мы написали! Об этом мы расскажем в следующей статье, так что следите за новостями! 🙂

Оцените статью
devanswers.ru
Добавить комментарий