DEV Community

ivanleomk
ivanleomk

Posted on • Updated on

Part 1 : Redesigning my Website with the t3-stack

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

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.

Screenshot 2022-07-27 at 2 32 18 PM

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(),
});
...

Enter fullscreen mode Exit fullscreen mode

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(),
});

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Screenshot 2022-07-27 at 2 25 58 PM

You'll then want to click on Personal Access Tokens

Screenshot 2022-07-27 at 2 27 21 PM

Lastly, you'll need to then configure your access token so it only has access to public_repo as seen blow

Screenshot 2022-07-27 at 2 30 19 PM

You should then update this token in your local .env file

Screenshot 2022-07-27 at 2 31 03 PM

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;
Enter fullscreen mode Exit fullscreen mode

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}`,
    };
  },
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

This gives us the following output in our console, indicating that we have successfully linked the frontend and our backend.

Screenshot 2022-07-27 at 2 51 17 PM

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;
};
Enter fullscreen mode Exit fullscreen mode

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";
    });
};
Enter fullscreen mode Exit fullscreen mode

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 }];
  };
};
Enter fullscreen mode Exit fullscreen mode

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,
    };
  },
});

Enter fullscreen mode Exit fullscreen mode

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"]);
Enter fullscreen mode Exit fullscreen mode

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"
                }
            ]
        }
    },
    ...
]
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

which in turn produces

Screenshot 2022-07-27 at 4 01 24 PM

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

Top comments (0)