Tutorial Next.JS + Tailwind : créer un blog
David Hockley
Posted on May 6, 2021
Je commence par installer Next.JS.
npx create-next-app nextjs-blog
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
Ensuite c'est parti pour installer et initialiser Tailwind :
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
Puis j'édite le fichier globals.css pour inclure les fichiers de style de Tailwind.
/* ./styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
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;
}
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;
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
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;
}));
}
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),
};
}
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);
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/></>
})}
</>
}
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),
};
}
Ensuite on définit dans types un paramètre
interface StaticRouteProps {
params: {
id: string;
};
}
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,
}
};
}
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),
};
}
A présent dans le blog on définit la fonction getStaticPaths
export async function getStaticPaths() {
const paths = BlogData.getAllIds();
return {
paths,
fallback: false
};
}
A présent tout est prêt pour faire la page :
On commence par importer Head, tout en haut :
import Head from 'next/head';
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;
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/></>
})}
</>
}
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
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')],
}
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
---
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;
}
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>
</>;
};
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';
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>
</>;
}
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 !
Posted on May 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.