Web development

Launching of my blog!

May 18, 2021 — 15 minutes read

From the choice of the platform, to the way I manage my data with DatoCMS, or to talk about the various difficulties I've faced. I give you, in this article, an in-depth review about how I launched my blog, antoinelin.com.


After more than 3 years of "website under construction", my blog is finally online. Building a website means building a space that is supposed to match your personality. A small piece of the Internet that belongs to you on which you want to express yourself. Whether it is through the content, but also through the web design, or the attention you give to details, it is your online presence that people will tends to judge. Subtly, but discernibly, every decision you make and every element you show matters in way. And if you're a freelancer, it's also a marketing showcase you're building.

The site where you are reading these few lines represents, for me, the result of a deep introspection. From the first mockup, to the choice of which CSS in JS library I used, or to talk about the difficulties I encountered. I'd like to come back, in this article, on why and how I designed and developed my website antoinelin.com.

Use an all-in-one solution, or do it from scratch ?

If you have ever considered creating a blog, and especially if you are a developer, you may have already asked yourself, as I did, which solution to choose when it comes to online publishing platforms. Develop a website, from scratch, independently? Use a WordPress theme? Or use a service such as Medium to not "reinvent the wheel" and benefit from its sharing ecosystem? At the end of the day, the time it takes to design and develop your website is the time it takes to create content, isn't it?

Personally, I wanted my platform to be an expression of my personality. So I rejected the solution to use WordPress theme which represents an hindrance to creativity. And I generally never use this kind of solutions as I prefer to have control over the technology to ensure the quality of the code.

Regarding services like Medium, my thoughts are that this represents the equivalent of fast-foods for online editorial content. And I don't want my articles to get lost among billions others on these platforms.

Greetings Mr. Lin. Here are the five posts of the day. Have you finished reading them? You can have one more for dessert. You don't like this one? No problem, you can swipe to the next one with just a finger movement.

Finally, creating your website is about dealing with fundamentals of a "classic" web project. UX, UI, SEO, content, and for the more techy ones, it's a way to show off your code outside of Git repositories. In my case, it's also a showcase for my clients. By this way, I prove them that I am able to develop a product, nothing more useful to prove that I am legitimate to build theirs.

If I had to summarize, it's mainly by independence criterion that I chose not to use a theme or a service. Many developers think that taking the time to create their own site is an unnecessary extra work. What they don't understand is that by doing so, they have the opportunity to stand out from other profiles.

The drawback is that it took me several months to finish it. Because of my desire to do it (too much) well, I had to go through many iterations on the graphic part and many hesitations on the choice of technologies.

So, develop a blog from scratch, use a pre-made theme, or use a dedicated service? You have my opinion on the question.

What are the purposes of this blog ?

Good, so I chose to develop a blog, well, but what makes it relevant is above all its content, don't you think so? Here is a short list of the different kind of posts you can find here:

  • Editorial content on the world of web development,

  • Tutorials, in the form of step-by-step solutions to specific problems,

  • News about my professional activity,

  • Feedback on my experiences and on the difficulties I have faced in my young career as a web developer,

  • Write, from time to time, moody posts on subjects that may affect me.

In general, I would like this blog to be dealing with web development and technology subjects. But I also want this platform to be a place of personal expression, where I feel free to publish whatever I want.

Also, you will find on my YouTube channel videos related to some of my posts. Most of the time, you'll have the video at the end of the page ! 👀

Illustration image by Christopher Gower - Unsplash

And tech wise, how does it work ?

Quick look at the stack


The website was built in TypeScript and React.js with the framework Next.js.
By default, Next.js performs a "pre-rendering" step of each page: it generates the content ahead of time via two different solutions, at your discretion. The first is static generation (SSG), which retrieves the data and passes the content when the website is building. And the second one, is the server-side rendering, which pre-renders a page, on the server side, at runtime. For this blog, I opted for static generation (SSG) in order to limit server load peaks and optimize rendering performance. If you want to know more about it, I invite you to read this page of the official Next.js documentation.

For styling, I use TailwindCSS in preview mode with the Just-in-time option enabled. For the moment, I write my style in inline (directly written in the virtual DOM of React) but it should go through CSS Modules soon. And to compose my classNames I use the eponymous utility : classNames.

Finally, the animations are made with Framer Motion for the DOM manipulation and with Paper.js for the "iPad OS" like cursor.


For the back-end, Next.js provides API routes accessible using REST protocol in which we can declare code that will run on the server side. Very useful if you want to do persistence with a database or call third party services without bundling your tokens in the front-end code. Talking about third party services and for the content management, I use an Headless CMS called DatoCMS and I get the data in GraphQL with the Urql library. All my logic is written using streams with RxJS and I use the following APIs to display the different small components on the website:

  • Spotify API for the component in the footer that displays the music I'm listening on Spotify,
  • Twitch API to display the banner that warns you when I'm streaming on Twitch,
  • Google Analytics to get and display the number of views of each of my articles.

To reduce the number of API calls above and to reduce the time of my requests between my front-end and my back-end, I added persistence with a normalized cache lru-cache.


This blog is deployed with Vercel. Having tried the first version when the service was still called Now, I must say that the product has changed a lot since then. Super simple to set up and pretty well optimized with Next.js, every time I edit code and push it to Github, the website is automatically deployed and updated in production. As fan of Kubernetes-like orchestration solutions and automated CI/CD, I was looking for a solution less overkill for a blog, I'm very satisfied with Vercel.

Third party services

To analyze user behavior, I use the following third-party services:

Data management, flow and display

I use an Headless CMS to manage the content of my blog: DatoCMS.

Do you see WordPress? DatoCMS is like the same, except that I don't need to host it and I only have an administration interface with which I add my content I fetch through API requests. This service has the great advantage of giving me the freedom to use any technology I want for the front-end.

With DatoCMS, I manage :

  • My posts,

  • The structure and content of each page,

  • The meta, global to the site, and per page.

I even have a connection via webhook with Vercel to deploy my site.

In the same way as WordPress, DatoCMS allows me to create my different data models. Here are the ones I use for the blog:

The data models I use for my blog
  • all_posts
    , where I define the structure of my
  • category
    , where I define all my categories of posts, with title and color,
  • home
    , where I define the structure of my home page
  • legal
    , same thing for the legal page
  • navigation
    , to edit my geographical location and my first availability,
  • post
    , data model for my posts,
  • question
    , for the FAQ questions I add at the end of each post,
  • single_post
    , for the structure of my
    page, where I put the content that should be the same for each post (the most popular posts, the top post, etc.).

Like an ACF for WordPress, I can define the different fields of my data model. Here is the set of field types that DatoCMS provides:

The different kind of fields you have with DatoCMS

Now I just have to create or modify content:

Add, modify, or delete a post
Post edition

After that, I just have to prepare my GraphQL request to fetch my post:

# SinglePost.query.graphql

query SinglePost($locale: SiteLocale!, $slug: String!) {
site: _site {
favicon: faviconMetaTags {
post(locale: $locale, filter: {slug: {eq: $slug}}) {
seo: _seoMetaTags {
_allSlugLocales {

At any time, I can explore my query options using the GraphiQL editor integrated in DatoCMS (the API Explorer tab in the navigation bar)

API Explorer de DatoCMS

Then, I just have to get this content and display it in my front-end.

Working with TypeScript, I use a

command which launches an introspection of my remote GraphQL server (i.e. my GraphQL DatoCMS endpoint) to retrieve the schema and use GraphQL Code Generator to transform this schema into TypeScript typing.
If you're interested by this topic, I'll do a full tutorial dedicated to running an Headless CMS with GraphQL and TypeScript.

As explained above, I use the SSG (static content generation) solution with Next.js which allows me to retrieve my content at website build time. I then need to use a

function in the page I'm interested in which will initialize my GraphQL client (Urql) and execute the previous request:

// pages/posts/[post_slug].tsx

export const getStaticProps: GetStaticProps = async ({ params, preview = false, locale = 'fr' }) => {
const ssrCache = ssrExchange({ isClient: false })
const client = initGraphQLClient({ preview, ssrCache })
const slug = params?.post_slug

const { data, error } = await client
.query<SinglePostQuery>(SinglePostDocument, {

if (error || !data || !data.post) {
return {
notFound: true,

return {
props: {
urqlState: ssrCache.extractData(),
data: data ?? null,
previewTitle: data?.post?.title ?? 'Single Post',
revalidate: 1,

Then define the URLs that need to be processed and statically generated by Next.js using


// pages/posts/[post_slug].tsx

export const getStaticPaths: GetStaticPaths = async () => {
const client = initGraphQLClient({})

const { data: frData } = await client
.query<AllPostQuery>(AllPostDocument, {
locale: 'fr',

const { data: enData } = await client
.query<AllPostQuery>(AllPostDocument, {
locale: 'en',

const formatPath = (post: { slug: string }) => ({
params: {
post_slug: post.slug,

const frPaths = (frData?.allPosts as { slug: string }[]).map(formatPath) ?? []
const enPaths = (enData?.allPosts as { slug: string }[]).map(formatPath) ?? []

return {
paths: [...frPaths, ...enPaths],
fallback: true,

Next.js already gets the current route

, what it needs is to be given the list of slugs that correspond to the posts that need to be generated. To avoid having to restart the whole website build each time you add a post, you just have to add
fallback: true
in the response, which will launch a static generation for this page only when an user visit it for the first time.

Note: indexing robots (like the Google's one) trigger fallback when a page has not been generated at build time. If you have problems indexing your pages (bad URL, strange canonical tags, etc.) check if the error does not come from there.

OK. Now you know how I manage and retrieve data from my blog. Behind that, I do some processing for "rich" text content only using Remark to transform what I get in Markdown from DatoCMS to HTML.

Note: we'll talk about this in a dedicated post, but if you want to inject HTML in your Markdown content that should be used as such, make sure to "sanitize" your strings to protect yourself from XSS attacks or other. I advise you to use isomorphic-dompurify with Next.js, which is a

variant (that works on both server and client side) of DOMPurify.

Some more details

Color scheme and motion preferences

The site relies on the

media features to display the default color scheme and to disable, or not, the iPad OS like cursor as well as animations that may cause motion sickness.

To choose the color scheme:

useEffect(() => {
const getColorScheme = (event: MediaQueryListEvent) => {
const newColorScheme = event.matches ? 'dark' : 'light'


if (isBrowser && window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', getColorScheme)

return () => {
if (window.matchMedia) {
.matchMedia('(prefers-color-scheme: dark)')
.removeEventListener('change', getColorScheme)
}, [])

A few things to note:

  • I assign a CSS class to the

    based on the theme choice,

  • I store the user's choice in

    to do persistence,

  • I get the browser preference in

    to ensure the presence of the

  • I manage the color theme preference in a React


For the theme definition, I have a

file that contains the classes I assign to the
. In each class, I use CSS variables that are used in the configuration file

/* themes.css */

.theme-light {
--color-bg-primary: #ffffff;

.theme-dark {
--color-bg-primary: #131313;
// tailwind.config.js

module.exports = {
theme: {
backgroundColor: {
primary: 'var(--color-bg-primary)',

It remains to use it in my React DOM with TailwindCSS :

<div className="bg-primary">

The management of motion preferences works the same:

useEffect(() => {
const getMotionPreference = (event: MediaQueryListEvent) => {
const newMotionPreference: ReduceMotionPreference = event.matches ? 'no-preference' : 'reduce'


if (isBrowser && window.matchMedia) {
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', getMotionPreference)

return () => {
if (window.matchMedia) {
.matchMedia('(prefers-reduced-motion: reduce)')
.removeEventListener('change', getMotionPreference)
}, [])

A few things to note:

  • Like scheme management, I assign a CSS class to the

    based on the user's preferences,

  • Like scheme management, I manage the logic in a React

    to retrieve the value in each component easily,

  • The value of the preference is stored in a

    passed by the
    , as well as a method to modify it.

I just have to replace movement like animations (

). An example with the component I use to manage my text animations:

import { useContext } from 'react'

export default function SplitText({ text, ...props }: Props) {
const { motionPreference } = useContext(MotionPreferenceContext)
const words = text.split(' ')

return (
{words.map((word, i) => (
<span key={`word-${i}`} className="inline-block overflow-hidden">
initial: {
y: motionPreference === 'reduce' ? 0 : '100%',
opacity: motionPreference === 'reduce' ? 0 : 1,
show: i => ({
y: 0,
opacity: 1,
transition: {
delay: showDelay + i * 0.01,
stiffness: 500,
damping: 300,
hide: i => ({
y: motionPreference === 'reduce' ? 0 : '-105%',
opacity: motionPreference === 'reduce' ? 0 : 1,
transition: {
delay: i * 0.01,
stiffness: 500,
damping: 300,
style={motionPreference === 'no-preference' ? { willChange: 'transform' } : {}}
{word + (i !== words.length - 1 ? '\u00A0' : '')}


I haven't covered everything that has been done on this blog so as not to make a too long post. Once again, all the subjects I think are interesting like the management of website's internationalization, the connection with Spotify or the creation of the iPad OS like cursor, will benefit from a dedicated post. To make sure you don't miss anything, you can follow me on Twitter, I'll communicate on each article release.

I'd like to continue to improve this blog and develop new features. For now, I plan to add these updates:

  • A way to subscribe to an RSS feed to stay informed of upcoming article releases,

  • A solution to enlarge the images of the articles in "theater" mode,

  • Improvements concerning accessibility and optimization of the navigation for visually impaired people,

  • A progress bar for reading articles, and maybe an optional "automatic scrolling" mode,

  • Small updates to simplify navigation through the website (make the categories of articles clickable for example),

  • Transform the blog into a PWA to allow offline reading.

I hope you liked this first post, it's a new thing for me and I'm really looking forward to share many other topics with you.

If you have any opinions or feedback, you can use this form, and I'd be happy to discuss it with you on Twitter (Thanks in advance for your time 🙏🏻).

And if this article helped you in any small way, please let me know that too!

Cheers! 👋🏻

Rating: 4.5/5 with more than 3 votes.
Give me your feebackShare on Twitter

Related Q&A

  • Who am I ?

    My name is Antoine Lin, I'm 25 and I live near Paris. I grew up in Seine-et-Marne and I studied at HETIC, Montreuil. I'm a freelance developer, and I work with companies and influencers by creating tailored and optimized digital experiences helping them reaching online goals since 2015.

  • What content will I find on this blog?

    The kind of content you'll find on this blog is mostly related to web development and technology (editorial content, tutorials...). But I also want this platform to be a place of personal expression, where I feel free to publish whatever I want.

  • How was the site built?

    The site was built from scratch using TypeScript and React.js with the framework Next.js. For styling, I use TailwindCSS, and for animations Framer Motion. The website is deployed with Vercel.

Latest posts

Web development
How to use multiple GraphQL endpoints with Urql
June 02 2021 — min read

If you are familiar with GraphQL you should already have the need to query third party services data in parallel of your main GraphQL API. In this post I summary how I manage to handle multiple GraphQL API endpoints with a single Urql client.