DEV Community

Cover image for Creating a custom widget for Notion
Ramón Chancay 👨🏻‍💻
Ramón Chancay 👨🏻‍💻

Posted on • Updated on

Creating a custom widget for Notion

Hello, I'm back.

Today we are going to build a widget for Notion, using the dev.to API, to show the latest articles from our favorite authors.

✨ You can see the live demo at:
https://notion-widget-dev-to.vercel.app/?users=devrchancay,alexandprivate,dabit3

Disclaimer:

this project uses next, tailwind, typescript, NPM to generate a simple widget (This is overkill for this demo, I know) 😬

You know that you can achieve the same result with HTML + CSS + JavaScript. Maybe in the future I will add more widgets to justify the use of all those technologies.

To do it, we are going to use NextJS and TailwindCSS.

Start project [Nextjs]

To start the project, we execute the following command:

$ npx create-next-app dev-to-widget --use-npm -e with-typescript
Enter fullscreen mode Exit fullscreen mode

With that we already have the dev-to-widget directory, with a nextjs project, which uses npm and Typescript.

Add Tailwind to the project

We install the following dependencies:

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
Enter fullscreen mode Exit fullscreen mode

Then we generate the configuration files:

$ npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Now, we have the files tailwind.config.js and postcss.config.js in the root of the project.

Now, we modify "purge" in the tailwind settings, to include the page andcomponents directory, for when the build is generated to remove CSS that we are not using.

// tailwind.config.js
  module.exports = {
   purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
    darkMode: false,
    theme: {
      extend: {},
    },
    variants: {
      extend: {},
    },
    plugins: [],
  }
Enter fullscreen mode Exit fullscreen mode

And finally, we add tailwind in our pages/_app.tsx file.

import { AppProps } from "next/app";
import "tailwindcss/tailwind.css";

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

Next SSR

The widget works from a parameter in the URL called users that contains the usernames separated by a ,

For example:
?users=devrchancay,alexandprivate

With this parameter the widget will be rendered with "devrchancay" and "alexandprivate" in that order.

export const getServerSideProps = async ({ query }) => {
  const users = query?.users?.split(",") ?? [];

  const usersPromise = users.map((user) =>
    fetch(`https://dev.to/api/articles?username=${user}`).then((user) =>
      user.json()
    )
  );

  const blogPosts = await Promise.all(usersPromise);

  return {
    props: {
      blogPosts,
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

Let me explain:

  • I convert the string separated by ',' into an array.
 const users = query?.users?.split(",") ?? [];
// ['devrchancay', 'alexandprivate']
Enter fullscreen mode Exit fullscreen mode
  • Generated an array with the requests to the API with each user.
const usersPromise = users.map((user) =>
    fetch(`https://dev.to/api/articles?username=${user}`).then((user) =>
      user.json()
    )
  );

// [Promise<pending>(devrchancay), Promise<pending>(alexandprivate)]
Enter fullscreen mode Exit fullscreen mode
  • I resolve the promises and save them in an array that contains the articles of each author in the order that they were entered in the URL.
 const blogPosts = await Promise.all(usersPromise);
// [devrchancay-articles, alexandprivate-articles]
Enter fullscreen mode Exit fullscreen mode
  • I send the component to render the widget.
return {
    props: {
      blogPosts,
    },
  };
Enter fullscreen mode Exit fullscreen mode
  • And finally, we render the component.
const IndexPage = ({ blogPosts }) => {
  const router = useRouter();
  const usersQuery = router?.query?.users as string;
  const users = usersQuery?.split(",") ?? [];

  const [currentIndex, setCurrentIndex] = useState(0);

  const usersString = users.join(", ");

  return (
    <div>
      <Head>
        <title>Posts: {usersString}</title>
        <meta name="description" content={`dev.to posts ${usersString}}`} />
      </Head>

      <div className="max-w-xl mx-auto sm:overflow-x-hidden">
        {blogPosts[currentIndex]?.map((post) => (
          <div key={post.id} className="mb-4">
            {post.cover_image && (
              <div className="relative max-w-xl h-52">
                <Image src={post.cover_image} alt={post.title} layout="fill" />
              </div>
            )}
            <div className="py-2 px-2">
              <div>
                {post.tag_list.map((tag) => (
                  <a
                    key={tag}
                    target="_blank"
                    rel="noopener"
                    href={`https://dev.to/t/${tag}`}
                    className="mr-2"
                  >
                    #<span className="text-gray-900">{tag}</span>
                  </a>
                ))}
              </div>
              <h1 className="text-3xl tracking-tight font-extrabold text-gray-900 sm:text-4xl">
                {post.title}
              </h1>

              <p className="mt-3 text-xl text-gray-500 sm:mt-4">
                {post.description}
              </p>
              <a
                target="_blank"
                rel="noopener"
                className="text-base font-semibold text-indigo-600 hover:text-indigo-500"
                href={post.url}
              >
                Read full story
              </a>
            </div>
          </div>
        ))}
        <ul className="w-full overflow-x-scroll flex space-x-6 px-2 sticky bottom-0 bg-white z-50">
          {users.map((user, index) => (
            <li
              key={user}
              className={`py-2 ${
                currentIndex === index
                  ? "border-t-4 border-indigo-600"
                  : "border-t-4 border-transparent"
              } `}
            >
              <a
                href="/"
                className="text-center"
                onClick={(evt) => {
                  evt.preventDefault();
                  setCurrentIndex(index);
                }}
              >
                {user}
              </a>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

The widget working!

I use this widget in my personal workspace.

They should add it as an embed and resize it to their liking.

Notion widget

you can see the complete code in the repository: https://github.com/devrchancay/notion-widget-dev-to/blob/main/pages/index.tsx

Discussion (0)