Краткое введение
Это серия статей блога, в которых подробно описывается, как я настраиваю свой сайт с помощью стека t3. Если вы не читали предыдущие статьи, рекомендую посмотреть
- Часть 1 : Редизайн моего сайта с помощью t3-stack
- Часть 2 : Отображение отдельных статей
- Часть 3 : Добавление оглавления к статьям нашего блога
- Часть 4 : Приведение в порядок конечного продукта
Вы можете посмотреть сайт здесь
Начальная установка
Недавно я наткнулся на стек t3, просматривая twitter, и решил поработать над его внедрением, создав новое приложение t3 с помощью пакета create-t3-app
npm. Однако, прежде чем я смог многое сделать, я обнаружил эту специфическую ошибку при сборке приложения на Vercel.
Немного покопавшись в репозитории, который создал create-t3-app
, я отследил ее до определенного файла, а именно ./src/env/schema.mjs
, который содержал строку кода в виде
// @ts-check
import { z } from "zod";
/**
* Specify your server-side environment variables schema here.
* This way you can ensure the app isn't built with invalid env vars.
*/
export const serverSchema = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "test", "production"]),
NEXTAUTH_SECRET: z.string(),
NEXTAUTH_URL: z.string().url(),
DISCORD_CLIENT_ID: z.string(),
DISCORD_CLIENT_SECRET: z.string(),
});
...
Затем она вызывалась в client.mjs
и server.mjs
в процессе сборки, что приводило к возникновению ошибки. Исправление описано ниже.
export const serverSchema = z.object({
// DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "test", "production"]),
// NEXTAUTH_SECRET: z.string(),
// NEXTAUTH_URL: z.string().url(),
// DISCORD_CLIENT_ID: z.string(),
// DISCORD_CLIENT_SECRET: z.string(),
});
Давайте начнем!
Вопросы Github как CMS
Для этой части мы будем использовать официальный npm-пакет Github octokit
, поэтому выполните следующую команду
npm install @octokit/graphql
прежде чем приступить к остальной части этого раздела
Настройка персонального маркера доступа
Примечание: Персональный маркер доступа используется Github для получения доступа к информации о ваших репозиториях и коде. Он понадобится вам для этого конкретного проекта, чтобы использовать Github Issues в качестве CMS.
Сначала зайдите на сайт github.com, перейдите к своим настройкам и нажмите на Developer Settings.
Затем вам нужно будет нажать на Personal Access Tokens
Наконец, вам нужно настроить токен доступа так, чтобы он имел доступ только к public_repo, как показано на рисунке.
Затем вам следует обновить этот токен в локальном файле .env.
Не забудьте перезапустить ваш сервер разработки, чтобы новый .env файл загрузился.
Настройка маршрутизатора TRPC
Если вы использовали create-t3-app
для настройки проекта, все должно быть готово. Давайте попробуем в этом случае сделать github-спецификацию. Перейдите в src/server/router/index.ts
и вы найдете следующий код
// src/server/router/index.ts
import { createRouter } from "./context";
import superjson from "superjson";
import { exampleRouter } from "./example";
import { protectedExampleRouter } from "./protected-example-router";
export const appRouter = createRouter()
.transformer(superjson)
.merge("example.", exampleRouter)
.merge("question.", protectedExampleRouter);
// export type definition of API
export type AppRouter = typeof appRouter;
Если вы когда-нибудь раньше использовали express, вы поймете, что TRPC работает очень похоже. Мы можем обеспечить модульное разделение отдельных маршрутов, просто работая с функцией
.merge
. Давайте создадим новый маршрутизатор для обработки наших проблем на github, как показано ниже
import { createRouter } from "./context";
import { z } from "zod";
export const githubRouter = createRouter().query("hello", {
input: z
.object({
text: z.string(),
})
.nullish(),
resolve({ input }) {
const { text } = input;
return {
greeting: `Hello from github router. You indicated ${text}`,
};
},
});
Теперь мы можем прикрепить его к нашему основному appRouter
в файле index.ts
, как показано ниже
// src/server/router/index.ts
import { createRouter } from "./context";
import superjson from "superjson";
import { githubRouter } from "./github-router";
export const appRouter = createRouter()
.transformer(superjson)
.merge("github.", githubRouter);
// export type definition of API
export type AppRouter = typeof appRouter;
и вызвать его на фронтенде, изменив index.tsx
следующим образом
import type { NextPage } from "next";
import Head from "next/head";
import { trpc } from "../utils/trpc";
type TechnologyCardProps = {
name: string;
description: string;
documentation: string;
};
const Home: NextPage = () => {
const hello = trpc.useQuery(["github.hello", { text: "from tRPC" }]);
console.log(hello?.data?.greeting);
return <>Welcome to Tailwind</>;
};
export default Home;
Это даст нам следующий вывод в консоли, указывающий на то, что мы успешно связали фронтенд и бэкенд.
Причина, по которой у нас четыре разных консольных журнала, заключается в том, что
create-t3-app
автоматически включает SSR для нас, поэтому содержимое автоматически отображается на сервере перед отправкой на фронтенд, таким образом, код фронтенда выполняется дважды.
Получение списка постов
Давайте сначала получим наш список выпусков с github. Это можно сделать с помощью следующего фрагмента кода
import { graphql } from "@octokit/graphql";
const graphqlWithAuth = graphql.defaults({
headers: {
authorization: `token ${process.env.GITHUB_TOKEN}`,
},
});
export const getPosts = async () => {
const { repository } = await graphqlWithAuth(`
{
repository(owner: <your github username>, name: <the repository you're importing the issues from>) {
issues(last: 50) {
edges {
node {
title
createdAt
labels(first: 3) {
nodes{
name
}
}
}
}
}
}
}
`);
console.log(repository);
return repository;
};
Примечание: Более подробную информацию об API вы можете найти в документации github.
В моем конкретном списке проблем у меня есть два тега draft
и published
. Я хочу получить список только тех проблем, которые помечены тегом published
. Мы можем сделать это, выполнив фильтр над возвращаемым объектом, который мы получаем
export const getPublishedPosts: () => Promise<githubPostTitle[]> = async () => {
const { repository } = await graphqlWithAuth(`
{
repository(owner: <your github username> , name: <your repository where the issues are in>) {
issues(last: 50) {
edges {
node {
title
createdAt
labels(first: 3) {
nodes{
name
}
}
}
}
}
}
}
`);
return repository.issues.edges
.map(({ node }) => {
return { ...node };
})
.filter((post: githubPostTitle) => {
return post.labels.nodes[0].name === "published";
});
};
Мы также добавляем определенный нами тип githubPostTitle
в код, как показано ниже
type githubPostStatus = "draft" | "published";
type githubPostTitle = {
createdAt: string;
title: string;
labels: {
nodes: [{ name: githubPostStatus }];
};
};
Теперь все, что нам нужно сделать, это вызвать это в нашем файле github-router.ts
, который хранит наш маршрутизатор github, и у нас есть маршрут, который мы можем вызвать из нашего фронтенда, который отображает наш список постов, как показано ниже.
*Примечание: * Маршрутизатор в данном случае — это просто набор запросов и мутаций, которые мы определяем. Каждый запрос и мутация имеют соответствующий метод resolve для получения определенного результата. Мы можем обеспечить безопасность типов API, используя
zod
, который имеет ряд функций-оберток для использования.
import { createRouter } from "./context";
import { z } from "zod";
import { getPublishedPosts, githubPostTitle } from "../../utils/github";
export const githubRouter = createRouter().query("get-posts", {
async resolve({ input }) {
const repositoryInformation: githubPostTitle[] = await getPublishedPosts();
return {
posts: repositoryInformation,
};
},
});
Рендеринг наших постов на фронтенде
Теперь мы можем вызвать наш конкретный маршрут с помощью пользовательского хука react useQuery
.
const posts = trpc.useQuery(["github.get-posts"]);
Это, в свою очередь, возвращает объекты, которые выглядят следующим образом
posts : [
{
"title": "Setting up a development enviroment",
"createdAt": "2022-07-27T06:14:56Z",
"labels": {
"nodes": [
{
"name": "published"
}
]
}
},
...
]
Обратите внимание, что как только мы добавили фильтр, все наши вопросы, помеченные как draft
, исчезли. Затем мы можем отобразить этот список постов на экране, чтобы отобразить их в полноэкранном режиме
import type { NextPage } from "next";
import Head from "next/head";
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 <p key={item.title}>{item.title}</p>;
})}
</>
);
};
export default Home;
что в свою очередь дает
Вуаля! Теперь мы подключили наши github issues к фронтенду NextJS с помощью TRPC.
В следующей части этой серии мы перейдем к написанию пользовательской страницы, которая сможет правильно отображать уценку на каждой конкретной странице.
Я опубликовал следующую часть этой серии, вы можете прочитать ее здесь