Build a Linktree clone in under 1 hour with Strapi, Next.js and GraphQL
Tags: strapi, nextjs, graphql, tailwindcss
Published: 2021-01-21 14:51:07

---
title: "Build a Linktree clone in under 1 hour with Strapi, Next.js and GraphQL"
published: true
description: "Building a basic Linktree clone seems rather simple which is perfect for showcasing a bunch of technologies and how they work together. Strapi, GraphQL and Next.js are the main building blocks for this project while Tailwind and GraphQL Codegen help with efficiency and developer experience."
tags: strapi, nextjs, graphql, tailwind
canonical_url: "https://pgvr.dev/blog/linktree-clone"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/i/w2cl9adpf7xmmirayurs.png"
---

Building a basic Linktree clone seems rather simple which is perfect for showcasing a bunch of technologies and how they work together. Strapi, GraphQL and Next.js are the main building blocks for this project while Tailwind and GraphQL Codegen help with efficiency and developer experience.

First, Strapi will be set up together with GraphQL and the necessary data models to have users and buttons. Second, Next.js with Typescript and the Apollo Client will be set up together with the GraphQL Code Generator, and finally, the UI will be built using Tailwind. Feel free to skip ahead as this project includes a lot of setting stuff up :)

The project is on [Github](https://github.com/patrickgoeler/linktree-clone) and I also made a [video on Youtube](https://youtu.be/InxEu7TfE7A) about this project, feel free to check those out.

## Backend with Strapi

To begin with I like to set up the project folder as a [yarn](https://classic.yarnpkg.com/en/docs/install) monorepo. To do that run these commands in your terminal to create a folder and initialize the project.

```bash
mkdir linktree-clone
cd linktree-clone
yarn init -y
mkdir packages
```

Every sub-folder like frontend, backend, ... will be in the `packages` folder. But first the `package.json` needs to be modified to make the _yarn workspaces_ work.

```json
{
  "name": "linktree-clone",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "private": true,
  "workspaces": ["packages/*"]
}
```

The important, new fields are **private** and **workspaces**, so make sure you have those.

Now Strapi can be installed, but make sure you are in the **packages** folder before installing it.

```bash
yarn create strapi-app backend --quickstart
```

The quickstart option gives us a _users and permissions_ out of the box. After installing Strapi will automatically launch and prompt you to create an admin user. Enter whatever you want here and you will be redirected to the Strapi home.

Now it is time to install the Strapi GraphQL plugin by going to the _marketplace_ and installing it. I had some trouble installing this plugin, my suspicion is that the yarn workspaces are messing this up somehow. What helped me is to stop Strapi, run `yarn` to install packages again, launch Strapi with `yarn develop` and install the GraphQL plugin again. This worked for me, however, I hope it works for you the first time. Be patient during the installation, the server should reload automatically once it is done.

Now let's create an application user since we only have an admin user at the moment.

![https://codingcastle.dev/static/images/linktree-clone/user-creation.jpg](https://codingcastle.dev/static/images/linktree-clone/user-creation.jpg)

Now that we have a user let's create the _buttons_ collection. Go to Content Types Builder and create a new collection type called **Button**. A basic button needs these fields:

-   text: text
-   url: text
-   visible: boolean
-   user: Many-to-one relation with user from user-permissions (not admin-user)

You can also set text and url as required in the _advanced field settings_ and provide a default value for visible. Save the collection and create a button which belongs to your user so we have some data to fetch. Don't forget to also publish the button after saving. Before fetching data the permissions for the buttons and users need to be adjusted. For development purposes we can allow all functions for these collections. For a real world scenario these would have to be double checked! So go to Settings → Roles → Public and allow everything for buttons and users (users are at the bottom).

![https://codingcastle.dev/static/images/linktree-clone/button-permissions.jpg](https://codingcastle.dev/static/images/linktree-clone/button-permissions.jpg)

The backend should now be ready for data to be fetched. The GraphQL playground can be found at "http://localhost:1337/graphql". Try this query which should give you your created button and username.

```graphql
query Buttons {
    buttons {
        text
        url
        user {
            username
        }
    }
}
```

And that's everything we need to set up a backend with Strapi. I love how easy it is to just start developing and not get bogged down by creating all the CRUD functions for the collections by yourself. The fact that users and permissons are pre-installed with the quickstarter are super nice too, even though the permissions don't really do anything _for now_. Plus, the GraphQL setup was super easy and now we have an always up-to-date GraphQL schema.

## Frontend with Next.js

Setting up Next.js is really straight forward. From within the **packages** folder a Next.js app with Typescript can be installed with:

```bash
yarn create next-app frontend --example with-typescript
```

The team behind Next.js provides a [long list of examples](https://github.com/vercel/next.js/tree/master/examples) to use different technologies with Next.js. Some examples are: Typescript, Tailwind, Apollo, pretty much all CMS's and more.

After the installation, open the frontend with VS Code (or preferred editor) and create this file inside the **lib** folder.

```tsx
import { useMemo } from 'react'
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'
import merge from 'deepmerge'

export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__'

let apolloClient: ApolloClient<any>

function createApolloClient() {
  return new ApolloClient({
    ssrMode: typeof window === 'undefined',
    link: new HttpLink({
      uri: 'http://localhost:1337/graphql', // Server URL (must be absolute)
    }),
    cache: new InMemoryCache(),
  })
}

export function initializeApollo(initialState: any = null) {
  const _apolloClient = apolloClient ?? createApolloClient()

  // If your page has Next.js data fetching methods that use Apollo Client, the initial state
  // gets hydrated here
  if (initialState) {
    // Get existing cache, loaded during client side data fetching
    const existingCache = _apolloClient.extract()

    // Merge the existing cache into data passed from getStaticProps/getServerSideProps
    const data = merge(initialState, existingCache)

    // Restore the cache with the merged data
    _apolloClient.cache.restore(data)
  }
  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') return _apolloClient
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient

  return _apolloClient
}

export function addApolloState(client: any, pageProps: any) {
  if (pageProps?.props) {
    pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract()
  }

  return pageProps
}

export function useApollo(pageProps: any) {
  const state = pageProps[APOLLO_STATE_PROP_NAME]
  const store = useMemo(() => initializeApollo(state), [state])
  return store
}
```

This is coming pretty much straight from the example repo _[with-apollo](https://github.com/vercel/next.js/tree/master/examples/with-apollo)._ Only the server URI is different and the function parameters are typed with any. The only thing left to do is to add the @apollo/client package. This file is more like a setup-once kind of thing, so I don't mind that the parameters are typed with any.

```bash
yarn add @apollo/client
```

Now the entire app needs be wrapped with the _ApolloProvider_ using the client we just created. In the pages folder create this special Next.js entry point file which wraps the entire application.

```tsx
import { ApolloProvider } from "@apollo/client";
import { AppProps } from "next/app";
import { useApollo } from "../lib/apolloClient";

export default function App({ Component, pageProps }: AppProps) {
  const apolloClient = useApollo(pageProps);
  return (
    <ApolloProvider client={apolloClient}>
      <Component {...pageProps}></Component>
    </ApolloProvider>
  );
}
```

At this point I also like to create all the files not being used at the moment. That would be everything inside the components folder, the interfaces folder, the utils folder, the api and users folder in pages and the about.tsx file.

### GraphQL Codegen

Since we have a complete GraphQL schema from Strapi the [GraphQL Code Generator](https://graphql-code-generator.com/) can generate a complete set of types and hooks for us to use. Just head to the [installation page](https://graphql-code-generator.com/docs/getting-started/installation) and follow the instructions to install all the dependencies. In short it's this:

```bash
yarn add graphql
yarn add -D @graphql-codegen/cli
yarn graphql-codegen init
```

The 3rd command initializes the repo. The schema is located at "../backend/exports/graphql/schema.graphql", operations and fragments should be at "lib/graphql/\*_/_.graphql" and the output should be "lib/graphql/output.tsx". An introspection file is not needed and I called the script that runs the codegen "gen". For the rest the default options are fine. You can check out [my video](https://youtu.be/InxEu7TfE7A) if you feel like these instructions are not clear enough or ask me about it in the comments.

Now the first query can be created in lib/graphql/ButtonsQuery.graphql.

```graphql
query Buttons {
  buttons {
    text
    url
    user {
      username
    }
  }
}
```

Now the _yarn gen_ command should generate types and more for this query but the first time I ran this command I got an error. After running _yarn_ again to install dependencies _yarn gen_ worked just fine. Keep in mind that every time the model or any .graphql files change the _gen_ command needs to run again.

### Tailwindcss

The [official guide by Tailwindcss](https://tailwindcss.com/docs/guides/nextjs) about the integration with Next.js is fantastic, so for the sake brevity I encourage you to follow the [official guide](https://tailwindcss.com/docs/guides/nextjs).

However, there is one thing I would like to add. The [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension for VS Code is a _must have_ in my opinion. This will give you auto-complete for Tailwind css classes which is especially great for beginners. If the intellisense doesn't work, try reloading the VS Code window. This usually fixes it.

### Fetching the data

Thinking about what actually needs to be displayed it is quite obvious that the content is mostly static. On every user page there are just a few buttons that may not change from build to build. So we can initially render everything on the server. Every user page is accessible at the /username route. In Next this can be achieved by creating a new page called `[username].tsx` . This means that every route, unless occupied by another page, will map every /username to this page and the username parameter will be available in the props.

However, we only want to render a user page for actual users of the application. Next provides a function that allows us to limit which values we want to be allowed to map to this username page.

```graphql
query Users {
  users {
    username
  }
}
```

In this case we can simply fetch all the users and only allow their usernames. Add a new query, run the code generator and add this function to the [username].tsx file.

```tsx
export async function getStaticPaths() {
  const apolloClient = initializeApollo();

  const { data } = await apolloClient.query<UsersQuery>({
    query: UsersDocument,
  });

  return {
    paths: data?.users?.map((user) => ({
      params: {
        username: user?.username,
      },
    })),
    fallback: false,
  };
}
```

This instantiates a new Apollo client, executes the users query and maps the usernames to the paths. So if there is **no** user with username "fred" and someone tries to access /fred then there will be a 404.

Now the button data needs to be fetched.

```tsx
export async function getStaticProps({ params }: GetStaticPropsContext) {
  const apolloClient = initializeApollo();

  const { data } = await apolloClient.query<ButtonsQuery>({
    query: ButtonsDocument,
    variables: { username: params?.username },
  });

  return {
    props: { data, username: params?.username },
    revalidate: 1,
  };
}
```

This looks really similar to the previous function. It receives the username as a parameter and we can use that to look up what buttons belong to that user. Then the button data and the username are returned as props to the component.

To filter the buttons by username the ButtonsQuery has to be adjusted though.

```graphql
query Buttons($username: String!) {
  buttons(where: { user: { username: $username } }) {
    text
    url
    user {
      username
    }
  }
}
```

With this data now being efficiently fetched on the server and available in the props, this project is now ready to tackle the (little) UI work there is.

```tsx
type Props = {
  data: ButtonsQuery;
  username: string;
};

export default function Username({ data, username }: Props) {
  return (
    <div className="min-h-screen max-w-2xl mx-auto flex flex-col items-center py-10">
      <img
        className="rounded-full h-24 w-24 mb-4"
        // the source is a base64 string but dev.to doesn't allow these kind of strings it seems
        src=""
      />
      <h1 className="font-bold text-lg mb-10">@{username}</h1>
      <div className="space-y-4 w-full">
        {data.buttons?.map((btn) => (
          <Button key={btn?.text} text={btn?.text!} url={btn?.url!} />
        ))}
      </div>
      <div className="flex-1"></div>
      <svg
        className="w-28"
        display="block"
        height="100%"
        width="100%"
        data-testid="BlackGreenLogo"
        viewBox="0 0 137 25"
      >
        <title data-testid="svgTitle" id="title_0.0374982482169921">
          title
        </title>
        <desc data-testid="svgDescription" id="description_0.0374982482169921">
          description
        </desc>
        <g>
          <path
            d="m37.1 24.4v-20.8c0-0.3 0.2-0.6 0.6-0.6h1.9c0.3 0 0.6 0.2 0.6 0.6v20.9c0 0.3-0.2 0.6-0.601 0.6h-1.899c-0.3-0.1-0.6-0.3-0.6-0.7z"
            fill="#3D3B3C"
          ></path>
          <path
            d="m44.3 5.5v-2c0-0.3 0.2-0.6 0.6-0.6h2.1c0.3 0 0.6 0.2 0.6 0.6v2c0 0.3-0.2 0.6-0.6 0.6h-2.1c-0.4 0-0.6-0.2-0.6-0.6zm0.1 18.9v-14.8c0-0.3 0.3-0.6 0.6-0.6h1.8c0.3 0 0.6 0.2 0.6 0.6v14.9c0 0.3-0.199 0.6-0.6 0.6h-1.8c-0.3-0.1-0.6-0.3-0.6-0.7z"
            fill="#3D3B3C"
          ></path>
          <path
            d="m51.5 24.4v-14.8c0-0.3 0.2-0.6 0.6-0.6h1.8c0.3 0 0.6 0.2 0.6 0.6v1h0.1c0.6-0.8 1.6-1.5 3.1-1.9h0.1c2.3-0.3 4.1 0.2 5.3 1.5 1 1.1 1.601 2.5 1.601 4.399v9.899c0 0.3-0.2 0.601-0.601 0.601h-1.8c-0.3 0-0.6-0.199-0.6-0.601v-9.101c0-2.699-1.101-4.101-3.4-4.101-1.1 0-2.1 0.3-2.8 1s-1 1.601-1 2.7v9.5c0 0.3-0.2 0.6-0.601 0.6h-1.8c-0.399-0.096-0.599-0.296-0.599-0.696z"
            fill="#3D3B3C"
          ></path>
          <path
            d="m68.2 24.4v-20.8c0-0.3 0.2-0.6 0.6-0.6h1.8c0.3 0 0.6 0.2 0.6 0.6v12.6h0.1l6.3-6.9c0-0.2 0.2-0.3 0.3-0.3h2.3c0.5 0 0.7 0.6 0.399 0.9l-4.599 5.2c-0.2 0.2-0.2 0.5 0 0.7l5.6 8.4c0.2 0.399 0 0.899-0.5 0.899h-2c-0.199 0-0.398-0.101-0.5-0.199l-5.1-7.6h-0.1l-2.1 2.3c-0.1 0.102-0.1 0.2-0.1 0.4v4.6c0 0.3-0.2 0.6-0.6 0.6h-1.8c-0.3-0.2-0.6-0.4-0.6-0.8z"
            fill="#3D3B3C"
          ></path>
          <path
            d="M85.3,20v-8.4h-1.8c-0.3,0-0.602-0.2-0.602-0.6V9.6C83,9.3,83.2,9,83.5,9h1.9V5.7   c0-0.3,0.199-0.6,0.601-0.6h1.7c0.3,0,0.6,0.2,0.6,0.6V9h3.4c0.3,0,0.6,0.2,0.6,0.6V11c0,0.3-0.199,0.6-0.6,0.6h-3.4v8.1   c0,0.899,0.1,1.5,0.4,1.899C89,22,89.5,22.2,90.3,22.2H91c0.3,0,0.6,0.2,0.6,0.6v1.7c0,0.3-0.199,0.6-0.6,0.6h-0.9   c-1.699,0-2.898-0.398-3.699-1.199C85.7,22.9,85.3,21.7,85.3,20z"
            fill="#3D3B3C"
          ></path>
          <path
            d="M95.5,24.4V9.6C95.5,9.3,95.7,9,96,9h1.7c0.3,0,0.6,0.2,0.6,0.6v1.2H98.4c0.301-0.6,0.801-1.1,1.399-1.5   c0.601-0.4,1.2-0.5,1.9-0.5c0.3,0,0.698,0,1,0.1C103,9,103.1,9.3,103.1,9.5l-0.3,1.8c-0.1,0.3-0.399,0.5-0.7,0.4   c-0.199,0-0.398-0.1-0.699-0.1c-1.899,0-2.899,1.3-2.899,3.9v8.8c0,0.3-0.2,0.601-0.601,0.601H96C95.7,25,95.5,24.8,95.5,24.4z"
            fill="#3D3B3C"
          ></path>
          <path
            d="m116.6 19.8 1.301 1c0.199 0.2 0.301 0.5 0.101 0.7-0.601 0.9-1.399 1.7-2.3 2.4-1.102 0.699-2.5 1.1-4 1.1-2.2 0-4-0.8-5.4-2.3s-2.1-3.601-2.1-6.101c0-2.601 0.699-4.601 2.1-6.101s3.1-2.3 5.301-2.3c2.101 0 3.899 0.8 5.199 2.3 1.301 1.5 2 3.601 2 6.2v0.302c0 0.3-0.199 0.6-0.6 0.6h-10.4c-0.3 0-0.6 0.301-0.6 0.602 0.1 1.1 0.5 2.1 1.1 2.898 0.801 0.9 1.801 1.4 3.2 1.4 0.7 0 1.3-0.1 1.8-0.3s1-0.5 1.301-0.8c0.399-0.4 0.601-0.7 0.799-0.9 0.102-0.1 0.201-0.3 0.302-0.4 0.196-0.4 0.596-0.5 0.896-0.3zm-9.4-4.7h8.6c-0.1-1.199-0.399-2.3-1.1-3.1-0.7-0.9-1.802-1.3-3.2-1.3-1.3 0-2.4 0.5-3.102 1.4-0.798 0.9-1.198 1.9-1.198 3z"
            fill="#3D3B3C"
          ></path>
          <path
            d="m134 19.8 1.3 1c0.2 0.2 0.3 0.5 0.101 0.7-0.601 0.9-1.399 1.7-2.301 2.4-1.1 0.699-2.5 1.1-4 1.1-2.199 0-4-0.8-5.398-2.3-1.4-1.5-2.102-3.601-2.102-6.101 0-2.601 0.7-4.601 2.102-6.101 1.398-1.5 3.1-2.3 5.3-2.3 2.101 0 3.899 0.8 5.2 2.3 1.3 1.5 2 3.601 2 6.2v0.302c0 0.3-0.2 0.6-0.602 0.6h-10.4c-0.302 0-0.601 0.301-0.601 0.602 0.101 1.1 0.5 2.1 1.101 2.898 0.8 0.9 1.8 1.4 3.198 1.4 0.701 0 1.302-0.1 1.802-0.3s1-0.5 1.3-0.8c0.398-0.4 0.6-0.7 0.8-0.9 0.101-0.1 0.2-0.3 0.3-0.4 0.2-0.4 0.6-0.5 0.9-0.3zm-9.4-4.7h8.602c-0.102-1.199-0.4-2.3-1.102-3.1-0.699-0.9-1.8-1.3-3.199-1.3-1.301 0-2.399 0.5-3.101 1.4-0.8 0.9-1.2 1.9-1.2 3z"
            fill="#3D3B3C"
          ></path>
          <path
            d="m11 0.7c-0.5-0.9-1.8-0.9-2.3 0l-8.6 15.6c-0.4 0.8 0.2 1.7 1.1 1.7h5.8v5.9c0 0.6 0.5 1.1 1.1 1.1h3.4c0.6 0 1.1-0.5 1.1-1.1v-5.9h-1.6c-0.7 0-1.2-0.5-1.3-1.1 0-0.2 0-0.4 0.1-0.602l4.8-8.7-3.6-6.898z"
            fill="#39E09B"
          ></path>
          <path
            d="m18.6 0.7c0.5-0.9 1.8-0.9 2.3 0l8.6 15.6c0.4 0.8-0.2 1.7-1.1 1.7h-5.7v5.9c0 0.6-0.5 1.1-1.101 1.1h-3.599c-0.6 0-1.1-0.5-1.1-1.1v-5.9h1.6c0.7 0 1.2-0.5 1.3-1.1 0-0.2 0-0.4-0.1-0.602l-4.8-8.698 3.7-6.9z"
            fill="#28BF7B"
          ></path>
        </g>
      </svg>
    </div>
  );
}
```

For the process and my thoughts behind building the UI I think [a video is suited way better than a blog post](https://youtu.be/InxEu7TfE7A), especially because there is nothing _exciting_ happening in the UI. But here is the final Button component. To center a container in the middle of the screen a `max-width` can be used together `margin: auto` on the x axis. To center everything within that container `display: flex` and `flex-direction column` are suitable. The button component stretches the entire container, is green and the background changes to white with an animation while a green border remains. Things like the avatar picture or the Linktree logo can be "stolen" by inspecting their website, but since this is for educational purposes only, I hope they don't mind.

```tsx
import NextLink from "next/link";

type Props = {
  text: string;
  url: string;
};

export default function Button({ text, url }: Props) {
  const link = url.includes("http") ? url : `https://${url}`;
  return (
    <NextLink href={link}>
      <button className="bg-green-300 text-white border-green-300 border-2 py-4 px-14 w-full hover:bg-white hover:text-green-300 transition-all duration-100">
        {text}
      </button>
    </NextLink>
  );
}
```

Let's compare the final result to Linktree.

This is the official Linktree website:

![https://codingcastle.dev/static/images/linktree-clone/linktree-official.png](https://codingcastle.dev/static/images/linktree-clone/linktree-official.png)

And the final result looks something likes this:

![https://codingcastle.dev/static/images/linktree-clone/final-result.png](https://codingcastle.dev/static/images/linktree-clone/final-result.png)

So pretty close I would say 🎉

Note that this post did **not** go into detail about authentication and secure API permissions with Strapi, advanced GraphQL operations for e.g. modifying the buttons or logging in, or deploying the result. If you would like to see this project continue to address any of these topics, leave a comment and tell me what you would like to see. You can check out the [project repository](https://github.com/patrickgoeler/linktree-clone) if you like.

As you can tell I'm just getting started with this whole blogging thing, so leaving a comment or follow would mean the world to me. 🌎

That's it for now, thanks for reading!