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
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
Then we generate the configuration files:
$ npx tailwindcss init -p
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: [],
}
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;
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,
},
};
};
Let me explain:
- I convert the string separated by ',' into an array.
const users = query?.users?.split(",") ?? [];
// ['devrchancay', 'alexandprivate']
- 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)]
- 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]
- I send the component to render the widget.
return {
props: {
blogPosts,
},
};
- 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>
);
};
The widget working!
I use this widget in my personal workspace.
They should add it as an embed and resize it to their liking.
you can see the complete code in the repository: https://github.com/devrchancay/notion-widget-dev-to/blob/main/pages/index.tsx
Top comments (1)
Do you build and deploy the Next.js app to Vercel, and then you copy the page URL inside Notion?