DEV Community

David Hockley
David Hockley

Posted on • Edited on

Tutorial Next.JS + Tailwind : créer un blog

Alt Text

Je commence par installer Next.JS.

npx create-next-app nextjs-blog
Enter fullscreen mode Exit fullscreen mode

Je crée un fichier tsconfig.json pour indiquer qu'on veut etre sous Typescript (et j'installe directement Typescript)

touch tsconfig.json
yarn add --dev typescript @types/react
Enter fullscreen mode Exit fullscreen mode

Ensuite c'est parti pour installer et initialiser Tailwind :

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

Puis j'édite le fichier globals.css pour inclure les fichiers de style de Tailwind.

/* ./styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

J'aime bien changer les fichier index.js et app.js en .tsx (mais ça ne change pas grand chose)

Maintenant que l’initialisât ion est faite, posons les bases de notre blog.

La première étape consiste à définir le type de l'article de blog, au sens Typescript.
Pour cela je crée un dossier shared/, à la racine, dans lequel je met un fichier types.d.ts qui va servir à contenir les différents types. Je vais supposer qu'on veut charger d'autres types de données que juste des articles de blog (mais je ne sais pas encore quoi à ce stade, donc j'en fais peut etre un peu trop). Néanmoins je crée un type de base pour mon contenu, que je spécialise pour l'article de blog.

interface item {
  id: string|number;
  content: string;
};

interface BlogPost extends item {
  title: string;
}
Enter fullscreen mode Exit fullscreen mode

A présent je crée la page d'accueil du blog, en créant un fichier index.tsx, dans un dossier blog/ que je crée dans /pages. Ce fichier va servir à lister tous les articles de blog (quand il y en aura !)

// pages/blog/index.tsx

interface BlogPagesProps {
  blogs: Array<BlogPost>
}

const BlogPages:React.FC<BlogPagesProps> = ({blog}) => {
  return <>Blog Posts</>
}
export default BlogPages;
Enter fullscreen mode Exit fullscreen mode

A présent, allons chercher le contenu que nous allons lister. Pour ça, je commence par créer un dossier content, dans le quel je mets pour le moment un dossier blog/ (une fois de plus, ça suppose que j'ai d'autre contenu)

Comment est-ce que ça fonctionne ? Dans une page on peut définir une fonction getStaticProps. Cette fonction sert pour le rendu côté serveur. Elle sert à récupérer toutes les données statiques qui utiles pour rendre la page. Typiquement si on voulait faire un appel à une base de données pour lister le contenu pertinent, c'est ici qu'on le ferait. Dans notre cas, nous allons lister tous les fichiers markdown (en .md) qui sont dans le dossier content/blog, et c'est ça qui nous servira de contenu.

Le nom de fichier servira d'identifiant (de champ id), mais on veut pouvoir aussi définir un titre, qui servira dans la liste sur l'accueil du blog.

Un petit détail qui est sympa avec Markdown : on peut définir des (méta)données en entête, et les lire. Ca s'appelle le front matter. C'est typiquement avec ça qu'on va pouvoir dire qui est l'auteur, ou quelle est l'image qu'on veut mettre en avant.

Une lib existe pour les lire : gray-matter

Ajouter gray-matter :

yarn add gray-matter
Enter fullscreen mode Exit fullscreen mode

Créer les fonctions de récupération des données :

On va créer une fonction de récupération générique, qui lit un dossier et qui retourne bah.. un type de contenu.

On va commencer par lister et lire tous les fichiers md qui sont dans un dossier donné:

async function _getAllData<T extends item>(directory: string): Promise<Array<T>> {
  // Get file names under folder
  const fileNames = fs.readdirSync(directory);
  return await Promise.all (fileNames.map(async fileName => {
    // Remove ".md" from file name to get id
    const id = fileName.replace(/\.md$/, '');

    // Read markdown file as string
    const fullPath = path.join(directory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');

    // Use gray-matter to parse the post metadata section
    const matterResult = matter(fileContents);

    let content = matterResult.content;

    return {
      id,
      ...matterResult.data,
      content,
    } as unknown as T;
  }));

}
Enter fullscreen mode Exit fullscreen mode

Alors je crée une fonction "factory", qui me fabrique la fonction en fonction du répertoire injecté :

export function makeGetters<T extends item> (directory: string) {

  return {
    getAllData: async () => _getAllData<T>(directory),
  };
}
Enter fullscreen mode Exit fullscreen mode

En suite on crée la version spécifique pour le blog :

On crée un fichier /shares/blog.ts tout simple

import path from 'path';
import { makeGetters } from './content';

const directory = path.join(process.cwd(), 'content/blog');

export default makeGetters<BlogPost>(directory);
Enter fullscreen mode Exit fullscreen mode

Maintenant on référence cette page dans l'index du blog

import BlogData from '../../shared/blog';

export async function getStaticProps() {
  const blogs = await BlogData.getAllData();

  return {
    props: {
      blogs,
    }
  };
}

interface BlogPagesProps {
  blogs: Array<BlogPost>
}

const BlogPages:React.FC<BlogPagesProps> = ({blogs}) => {
  return <><h1>Blog Posts</h1>
    {blogs.map((itm) => {
      return <><a key={itm.id}>{itm.title}</a><br/></>
    })}

  </>
}


Enter fullscreen mode Exit fullscreen mode

Et on teste : wundershön

Et du coup maintenant on va aller créer la page qui va servir à faire les articles de blog eux mêmes

Pour ça on crée un fichier [id].tsx dans le dossier pages/blog

En suite on fait la fonction qui récupère les données statiques en fonction de l'id

Autrement dit cette fois ci au lieu de récupérer tous les fichiers on récupère le seul fichier dont l'id est dans le slug, le chemin.

async function _getItemData<T extends item> (directory: string, id: string, extended?: boolean): Promise<T> {

  const fullPath = path.join(directory, `${id}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  // Use gray-matter to parse the post metadata section
  const matterResult = matter(fileContents);
  let content = matterResult.content;

  return {
    id,
    ...matterResult.data,
    content,
  } as unknown as T;
}

/* *** */

export function makeGetters<T extends item> (directory: string) {

  return {
    getItemData: async (id: string) => _getItemData<T>(directory, id),
    getAllData: async () => _getAllData<T>(directory),
  };

}
Enter fullscreen mode Exit fullscreen mode

Ensuite on définit dans types un paramètre

interface StaticRouteProps {
  params: {
    id: string;
  };
}
Enter fullscreen mode Exit fullscreen mode

Et on crée le get static props de la page :


interface PageProps {
  data: BlogPost;
}

export async function getStaticProps({params}:StaticRouteProps) {
  const data = await BlogData.getItemData(params.id);
  return {
    props: {
      data,
    }
  };
}

Enter fullscreen mode Exit fullscreen mode

Ici il y a une petite particularité. Dans la mesure ou c'est un chemin dynamique, on a besoin de savoir quels sont tous les contenus qu'il faut générer. Pour ça on a une fontion "getStaticPaths" qui en gros liste tous les contenus

On retourne du coup dans content :

const _getAllIds = (directory: string) => {
  const fileNames = fs.readdirSync(directory);

  return fileNames.map(fileName => {
    return {
      params: {
        id: fileName.replace(/\.md$/, '')
      }
    };
  });
};

/***/

export function makeGetters<T extends item> (directory: string) {

  return {
    getAllIds: () => _getAllIds(directory),
    getItemData: async (id: string) => _getItemData<T>(directory, id),
    getAllData: async () => _getAllData<T>(directory),
  };

}
Enter fullscreen mode Exit fullscreen mode

A présent dans le blog on définit la fonction getStaticPaths

export async function getStaticPaths() {
  const paths = BlogData.getAllIds();
  return {
    paths,
    fallback: false
  };
}
Enter fullscreen mode Exit fullscreen mode

A présent tout est prêt pour faire la page :

On commence par importer Head, tout en haut :

import Head from 'next/head';
Enter fullscreen mode Exit fullscreen mode

Puis on crée la fin de la page :

interface BlogPageProps {
  data: BlogPost;
}

const BlogPage: React.FC<BlogPageProps> = ({data}) => {

  return <>
    <Head> <title> {data.title} </title></Head>
    <main>
      <h1>{data.title}</h1>
      <div>
        {data.content}
      </div>
    </main>
  </>;
};

export default BlogPage;
Enter fullscreen mode Exit fullscreen mode

Maintenant... il ne reste plus qu'à y accéder !

Mettons à jour la page index du blog :

const BlogPages:React.FC<BlogPagesProps> = ({blogs}) => {
  return <><h1>Blog Posts</h1>
    {blogs.map((itm) => {
      return <><a key={itm.id} href={'/blog/'+itm.id}>{itm.title}</a><br/></>
    })}

  </>
}
Enter fullscreen mode Exit fullscreen mode

On teste et ça marche mais ... c'est très moche. Heureusement on peut améliorer tout ça avec Tailwind, en ajoutant le plugin "typography" :

yarn add @tailwindcss/typography
Enter fullscreen mode Exit fullscreen mode

Ensuite on édite tailwind.config.js pour rajouter le plugin :

module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [require('@tailwindcss/typography')],
}
Enter fullscreen mode Exit fullscreen mode

C'est tout de suite nettement plus beau !

Rajouter une image

A présent j'aimerais bien rajouter une image. Dans NextJS les images sont prises depuis le dossier public. Donc je met l'image dans /public/blog

Je rajoute l'info dans le font matter:

---
title: "Tutorial Next.JS + Tailwind : créer un blog"
img: blog.jpg
---
Enter fullscreen mode Exit fullscreen mode

Je rajoute aussi l'information dans le type du blog. L'image est optionnelle donc je l'indique :

interface item {
  id: string|number;
  content: string;
};

interface BlogPost extends item {
  title: string;
  img?: string;
}
Enter fullscreen mode Exit fullscreen mode

Pour finir je rajoute l'image dans la page de l'article, en prenant en compte le fait que l'image peut etre absente (et donc je mets une condition sur l'image :

const BlogPage: React.FC<BlogPageProps> = ({data}) => {

  return <>
    <Head> <title> {data.title} </title></Head>
    <main>

      <article className="prose lg:prose-xl mx-auto mt-12">
       <h1>{data.title}</h1>
        {data.img && <img src={'/blog/'+data.img} />}
        <ReactMarkdown>{data.content}</ReactMarkdown>
      </article>
    </main>
  </>;
};
Enter fullscreen mode Exit fullscreen mode

Et voilà, on a un début de blog, qui a une bonne tête.

Etape finale : rajouter les méta datas

Un article de blog, c'est fait pour être partagé et découvert, non ? Mais pour que l'article soit bien compris par les moteurs de recherhe et bien formatté pour les réseaux sociaux, on peut rajouter des informations complémentaires ou "meta data".

Pour ça Next met à disposition le composant head.

On l'importe dans notre fichier [id].tsx

import Head from 'next/head';
Enter fullscreen mode Exit fullscreen mode

IL y a trois composants principaux aux metadatas : un titre, une image, une description. On a déjà les deux premiers, il nous manque le troisième. Du coup... on le rajoute dans le front-matter du markdown et dans le type de l'article de blog, comme on l'a fait pour l'image. Ensuite on rajoute les infos dans notre composant (ceux sous la forme 'og:xxxxx' sont le format OpenGraph de Facebook mais ils sont aussi lus par Twitter. Ca nous donne :

const BlogPage: React.FC<BlogPageProps> = ({data}) => {

  /* ** etc. *** */

  return <>
    <Head> 
        <title> {data.title} </title>
        <meta 
            name="description" 
            content={data.description}>
        <meta 
            property="og:title" 
            content={data.title} 
        >
        <meta 
            property="og:description" 
            content={data.description}
        >
{ {data.img} && <meta 
            property="og:image" 
            content={data.img}
        >}

    </Head>
    <main>
    {/* *** etc. *** */}
    </main>
   </>;
}
Enter fullscreen mode Exit fullscreen mode

In fine

Voilà, vous avez avec ça les moyens de mettre en place un blog avec NextJS et TailwindCSS. Bon il reste encore pas mal de mise en page à faire sur la page principale et sur la page de présentation de la liste des articles de blog, sans parler de la navigation une fois qu'on est sur l'article, mais vous avez normalement les armes nécessaires pour le faire, ou sinon... n'hésitez pas si vous avez des commentaires ou des questions !

Top comments (1)

Collapse
 
agilitycms_76 profile image
Agility CMS

Here is another simple way to create blog with Tailwand CSS and Next.js - this headless cms starter :) preview-agilitywebsitegatsby.gtsb....

The code itself takes advantage of powerful tools such Tailwind CSS, a simple and lightweight utility-first CSS framework, and next/image for automatic image optimization.

That's a lot of awesome technology in one starter project! Thankfully, it all works out of the box, and you can deploy it automatically to Vercel for free - no coding required.