Part 1 : Redesigning my Website with the t3-stack
ivanleomk
Posted on July 27, 2022
Quick Introduction
This is a series of blog posts which detail how I'm setting up my website with the t3 stack. If you haven't read the previous articles, I suggest looking at
- Part 1. : Redesigning My Website with the t3-stack
- Part 2 : Displaying Individual Articles
- Part 3 : Adding a table of contents to our blog articles
- Part 4 : Tidying Up the final product
You can see the website here
Initial Set-up
Recently I chanced upon the t3 stack when browsing upon twitter so I decided to work on implementing it by creating a new t3 app using the create-t3-app
npm package. However, before I could do much, I found this specific error when building the app on Vercel.
After doing a bit of sleuthing within the repo that create-t3-app
had created, I traced it down to a specific file, that is ./src/env/schema.mjs
which had a line of code as
// @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(),
});
...
This was then being called in client.mjs
and server.mjs
during the build process which led to the error being thrown. The fix is outlined below.
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(),
});
Let's get started!
Github Issues as a CMS
For this specific portion, we'll be using Github's official npm package octokit
so run the following command
npm install @octokit/graphql
before starting the rest of this section
Setting up a personal access token
Note : A personal access token is used by Github to allow you to access information about your repositories and code. You'll need it for this particular project in order to use Github Issues as a CMS
What you'll want to do is to first go to github.com and then navigate to your settings and click on Developer Settings
You'll then want to click on Personal Access Tokens
Lastly, you'll need to then configure your access token so it only has access to public_repo as seen blow
You should then update this token in your local .env file
Don't forget to restart your development server so that the new .env file is loaded in.
Configuring the TRPC router
If you've used the create-t3-app
in order to configure your project, everything should be set-up nicely for you. Let's try to make a github-specific in this case. Go to src/server/router/index.ts
and you'll find the following code
// 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;
If you've ever used express before, you'll realise that TRPC works remarkably similar. We can ensure a modular separation of individual routes by simply working with the
.merge
function. Let's create a new router to handle our github issues as seen below
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}`,
};
},
});
We can now attach this to our main appRouter
in our index.ts
file as seen below
// 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;
and call it on the frontend by modifying index.tsx
as
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;
This gives us the following output in our console, indicating that we have successfully linked the frontend and our backend.
The reason why we have four different console logs is because
create-t3-app
automatically enables SSR for us so the content is automatically rendered on the server before being sent to the frontend, thus your frontend code runs twice.
Getting a list of posts
Let's first get our list of issues from github. You can do so by using the following code snippet below
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;
};
Note : You can find more information on the API at the github documentation.
In my specific issue list, I have two tags draft
and published
. I only want to get a list of the issues that are tagged with the published
tag. We can do so by performing a filter over the returned object that we obtain
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";
});
};
We also add in a type we define called githubPostTitle
in the code as seen below
type githubPostStatus = "draft" | "published";
type githubPostTitle = {
createdAt: string;
title: string;
labels: {
nodes: [{ name: githubPostStatus }];
};
};
Now all we have to do is to call this in our github-router.ts
file which stores our github router and we have a route we can call from our frontend that displays our list of posts as seen below.
*Note: * A router in this case is simply a collection of queries and mutations that we define. Each query and mutation has an associated resolve method in order to get a specific output. We can ensure we have typesafe APIs by using
zod
which has a number of wrapper functions in order for us to use.
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,
};
},
});
Rendering our posts on the frontend
We can now call our specific route by using the custom react hook useQuery
const posts = trpc.useQuery(["github.get-posts"]);
This in turn returns objects that look like this
posts : [
{
"title": "Setting up a development enviroment",
"createdAt": "2022-07-27T06:14:56Z",
"labels": {
"nodes": [
{
"name": "published"
}
]
}
},
...
]
Notice that once we added the filter, all our issues tagged as draft
disappeared. We can then map over this list of posts to render if on the screen
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;
which in turn produces
Voila! Now we've hooked up our github issues to our NextJS frontend using TRPC.
In the next part of this series, we'll move on to writing out a custom page that will be able to display the markdown in each specific page properly.
I've published the next part of this series, you can read it here
Posted on July 27, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.