Créer un digital garden de A à Z avec Gatsby et Obsidian

Mis à jour le dimanche 26 décembre 2021 par johackim

I. Initialiser un projet Gatsby

Pour initialiser un projet Gatsby, il suffit d'installer node.js puis exécuter cette commande :

npm install --save gatsby react react-dom

Une fois installé, vous pouvez démarrer Gatsby avec la commande suivante :

./node_modules/.bin/gatsby develop

Puis ajoutez ces lignes dans votre fichier package.json afin d'avoir accès aux commandes de gatsby plus facilement :

"scripts": {
"build": "gatsby build",
"start": "gatsby develop",
"serve": "gatsby serve",
"clean": "gatsby clean"
}

Vous pouvez à présent exécuter la commande npm start pour démarrer Gatsby.

PS : Exécutez la commande npm init -f si votre fichier package.json n'existe pas.

II. Importer ses notes markdown

Après avoir Initialiser un projet Gatsby, créez un dossier content qui va contenir toutes vos notes au format markdown (ex: content/hello-world.md).

NOTE : Vous pouvez ouvrir se dossier avec Obsidian ou n'importe quel éditeur de fichier markdown pour éditer vos notes.

Installez et configurez le package gatsby-source-filesystem & gatsby-transformer-remark pour pouvoir detecter les fichiers markdown de votre dossier content dans Gatsby :

yarn add -D gatsby-source-filesystem gatsby-transformer-remark
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: 'gatsby-source-filesystem',
options: {
path: './content',
},
},
{
resolve: 'gatsby-transformer-remark',
},
],
};

Créez le fichier gatsby-node.js avec la configuration si dessous pour pouvoir créer des pages pour chacune de vos notes markdown :

// gatsby-node.js
exports.createPages = async ({ actions, graphql, reporter }) => {
const { createPage } = actions;
const result = await graphql(`
{
allMarkdownRemark {
edges {
node {
id
html
parent {
... on File {
name
}
}
}
}
}
}
`);
if (result.errors) {
reporter.panicOnBuild('Error while running GraphQL query.');
return;
}
const markdowns = result.data.allMarkdownRemark.edges;
const noteTemplate = require.resolve('./src/templates/noteTemplate.js');
markdowns.forEach(({ node }) => {
const { id, html } = node;
createPage({
path: `/${node.parent.name}`,
component: noteTemplate,
context: { id, html },
});
});
};
// src/templates/noteTemplate.js
import React from 'react';
export default function Template({ pageContext }) {
const { html } = pageContext;
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

Si vous avez une note hello-world.md dans votre dossier content et que vous vous rendez à l'adresse http://localhost:8000/hello-world, cela devrait afficher le contenu de votre fichier markdown.

III. Ajouter le support de la syntaxe markdown d'Obsidian

Les fichiers markdown dans Obsidian peuvent avoir une syntaxe spéciale propre à Obsidian :

  • [[Internal link]]
  • [[Internal link|With custom text]]
  • [[Internal link#heading]]
  • [[Internal link#heading|With custom text]]
  • [[Embed note]]
  • [[Embed note#heading]]

Cette syntaxe permet de relier des notes entre elles via des liens bidirectionnels ([[Internal link]]).

Pour ajouter le support de ces liens, j'ai créé un plugin gatsby-remark-obsidian.

Vous pouvez intégrer ce plugin avec Remark ou MDX :

// gatsby-config.js
plugins: [
{
resolve: "gatsby-transformer-remark",
options: {
plugins: [
{
resolve: 'gatsby-remark-obsidian',
},
]
}
},
],

Si vous utilisez MDX :

// gatsby-config.js
plugins: [
{
resolve: 'gatsby-plugin-mdx',
options: {
extensions: ['.md'],
gatsbyRemarkPlugins: [
{
resolve: 'gatsby-remark-obsidian',
},
],
},
},
],

PS : Si vous utilisez Next.js, j'ai créer un autre plugin.

IV. Créer un Graph Viewer comme Obsidian

Pour créer un Graph Viewer comme Obsidian dans Gatsby et afficher visuellement les liens des fichiers markdowns, il existe la librairie vis-network :

yarn add -D react-graph-vis gatsby-source-filesystem gatsby-transformer-remark gatsby-transformer-markdown-references
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: 'gatsby-source-filesystem',
options: {
path: './content',
},
},
{
resolve: 'gatsby-transformer-remark',
},
{
resolve: 'gatsby-transformer-markdown-references',
options: {
types: ['MarkdownRemark'],
},
},
],
};

Créez la page index.js et le composant graph.js.

// src/pages/index.js
import React from 'react';
import { graphql } from 'gatsby';
import Graph from '../components/graph';
const IndexPage = ({ data: { allMarkdownRemark } }) => {
const nodes = allMarkdownRemark.edges.map(({ node }) => node);
return (
<div style={{ height: '100vh' }}>
<Graph nodes={nodes} />;
</div>
);
};
export const query = graphql`
{
allMarkdownRemark {
edges {
node {
id
parent {
... on File {
name
}
}
inboundReferences {
... on MarkdownRemark {
parent {
... on File {
id
name
}
}
}
}
outboundReferences {
... on MarkdownRemark {
parent {
... on File {
id
name
}
}
}
}
}
}
}
}
`;
export default IndexPage;
// src/components/graph.js
import React, { useState } from 'react';
import Graph from 'react-graph-vis';
import { navigate } from 'gatsby';
export default ({ nodes = [] }) => {
const [network, setNetwork] = useState();
const sizes = { DEFAULT: 10, MEDIUM: 20, LARGE: 30 };
const graph = { nodes: [], edges: [] };
const options = {
nodes: {
color: '#000',
font: { color: '#000' },
shape: 'dot',
size: 10,
widthConstraint: { maximum: 300 },
},
layout: {
hierarchical: false,
},
edges: {
color: '#000',
arrows: { to: { enabled: false } },
},
interaction: {
hover: true,
tooltipDelay: 3000000,
},
physics: {
enabled: true,
solver: 'repulsion',
repulsion: {
nodeDistance: 200,
},
},
};
const computeSize = (refs) => {
if (refs >= 5) return sizes.MEDIUM;
if (refs >= 10) return sizes.LARGE;
return sizes.DEFAULT;
};
const createVisNode = (node) => {
let size = sizes.DEFAULT;
if (node.inboundReferences && node.outboundReferences) {
size = computeSize(node.inboundReferences.length + node.outboundReferences.length);
}
return {
id: node.parent.name,
name: node.parent.name,
size,
url: `/${node.parent.name}`,
label: node.parent.name,
title: node.parent.name,
};
};
nodes.forEach((node) => {
const { inboundReferences, outboundReferences } = node;
graph.nodes.push(createVisNode(node));
inboundReferences.forEach((inboundReference) => {
graph.edges.push({ from: inboundReference.parent.name, to: node.parent.name });
if (nodes.length === 1) {
graph.nodes.push(createVisNode(inboundReference));
}
});
outboundReferences.forEach((outboundReference) => {
graph.edges.push({ from: node.parent.name, to: outboundReference.parent.name });
if (nodes.length === 1) {
graph.nodes.push(createVisNode(outboundReference));
}
});
});
graph.nodes = [...new Set(graph.nodes)];
const events = {
click: (event) => {
const selectedNode = graph.nodes.find((node) => node.id === event.nodes[0]);
if (selectedNode) {
const { url } = selectedNode;
navigate(url);
}
},
blurEdge: () => {
network.canvas.body.container.style.cursor = 'default';
},
blurNode: () => {
network.canvas.body.container.style.cursor = 'default';
},
hoverNode: () => {
network.canvas.body.container.style.cursor = 'pointer';
},
};
return (
<Graph graph={graph} events={events} getNetwork={(n) => setNetwork(n)} options={options} />
);
};

À présent, si vous créer deux fichier markdowns, ils seront afficher dans votre Graph Viewer :

<!-- content/note1.md -->
Go to [[note2]]
<!-- content/note2.md -->
Hello world!

V. Créer automatiquement une table des matières des fichiers markdown

Après avoir Initialiser un projet Gatsby et importer vos fichiers markdown, vous pouvez créer automatiquement la table des matière d'un fichier markdown dans Gatsby.

// gatsby-node.js
exports.createPages = async ({ actions, graphql, reporter }) => {
const { createPage } = actions;
const result = await graphql(`
{
allMarkdownRemark {
edges {
node {
id
html
headings {
value
depth
}
parent {
... on File {
name
}
}
}
}
}
}
`);
if (result.errors) {
reporter.panicOnBuild('Error while running GraphQL query.');
return;
}
const markdowns = result.data.allMarkdownRemark.edges;
const noteTemplate = require.resolve('./src/templates/noteTemplate.js');
markdowns.forEach(({ node }) => {
const { id, html, headings } = node;
createPage({
path: `/${node.parent.name}`,
component: noteTemplate,
context: { id, html, headings },
});
});
};
// src/components/toc.js
import React from 'react';
import { Link } from 'gatsby';
import slugify from 'slugify';
export default ({ headings = [], depthMin = 1, className = '' }) => {
if (!headings.length) return false;
return (
<ul className={className}>
{headings.filter(({ depth }) => depth >= depthMin).map(({ value }) => {
const id = slugify(value, { lower: true, strict: true });
return (
<li key={value}>
<Link to={`#${id}`} title={value}>
{value}
</Link>
</li>
);
})}
</ul>
);
};
// src/templates/noteTemplate.js
import React from 'react';
import Toc from '../components/toc';
export default function Template({ pageContext }) {
const { html, headings } = pageContext;
return (
<>
<Toc headings={headings} depthMin={2} />
<div dangerouslySetInnerHTML={{ __html: html }} />
</>
);
}

Pour créer automatiquement les id sur chaque header, il existe le plugin gatsby-remark-autolink-headers :

yarn add -D gatsby-remark-autolink-headers
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: 'gatsby-source-filesystem',
options: {
path: './content',
},
},
{
resolve: 'gatsby-transformer-remark',
options: {
plugins: [
{
resolve: 'gatsby-remark-autolink-headers',
options: {
icon: false,
},
},
],
},
},
],
};

VI. Améliorer le SEO de votre digital garden

Pour améliorer le SEO d'un site Gatsby, il existe le plugin helmet qui permet d'overrider les balises meta de votre site :

yarn add -D react-helmet

Ensuite, créez le fichier src/component/seo.js qui génerera toutes les balises meta nécessaires pour un bon référencement :

// src/components/seo.js
import React from 'react';
import Helmet from 'react-helmet';
import { useLocation } from '@reach/router';
import { useStaticQuery, graphql } from 'gatsby';
export default ({ title, image, description, article, datePublished, dateModified, tags = [], meta = [] }) => {
const location = useLocation();
const { site } = useStaticQuery(graphql`
query {
site {
siteMetadata {
title
lang
logo
description
author
twitter
siteUrl
siteName
}
}
}
`);
const defaultTitle = site.siteMetadata?.title;
const defaultDescription = site.siteMetadata?.description;
return (
<Helmet
htmlAttributes={{ lang: site.siteMetadata.lang }}
title={title || defaultTitle}
meta={[
{
name: 'description',
content: description || defaultDescription,
},
{
property: 'og:url',
content: location.href,
},
{
property: 'og:site_name',
content: site.siteMetadata.siteName,
},
{
property: 'og:title',
content: title || defaultTitle,
},
{
property: 'og:description',
content: description || defaultDescription,
},
{
property: 'og:type',
content: article ? 'article' : 'website',
},
...(image ? [{
property: 'og:image',
content: image,
}] : []),
...(tags ? tags.map((tag) => ({
property: 'article:tag',
content: tag,
})) : []),
{
name: 'twitter:title',
content: title || defaultTitle,
},
{
name: 'twitter:creator',
content: site.siteMetadata.twitter,
},
{
name: 'twitter:site',
content: site.siteMetadata.twitter,
},
{
name: 'twitter:description',
content: description || defaultDescription,
},
...(image ? [{
name: 'twitter:image',
content: image,
}, {
name: 'twitter:card',
content: 'summary_large_image',
}] : [{
name: 'twitter:card',
content: 'summary',
}]),
].concat(meta)}
>
<script type="application/ld+json">
{JSON.stringify({
'@context': 'https://schema.org',
'@type': article ? 'Article' : 'WebSite',
mainEntityOfPage: {
'@type': 'WebPage',
'@id': site.siteMetadata?.siteUrl,
},
url: location.href,
headline: title || defaultTitle,
author: {
'@type': 'Person',
name: site.siteMetadata.author,
},
publisher: {
'@type': 'Organization',
name: site.siteMetadata.author,
url: site.siteMetadata?.siteUrl,
logo: {
'@type': 'ImageObject',
url: site.siteMetadata.logo,
},
},
description: description || defaultDescription,
...(image && { image }),
...(dateModified && { dateModified }),
...(datePublished && { datePublished }),
})}
</script>
</Helmet>
);
};

Vous pouvez gérer les données par défault de votre site dans le fichier gatsby-config.js :

// gatsby-config.js
module.exports = {
siteMetadata: {
title: 'Website Title',
siteName: 'Website Name',
description: 'Description',
author: 'author',
twitter: '@author',
logo: 'http://example.com/logo.png',
lang: 'fr',
siteUrl: 'https://example.com',
}
};

Vous pouvez Intégrer le composant SEO dans la page de votre choix (ex: index.js) :

// src/pages/index.js
import React from 'react';
import SEO from '../components/seo';
const IndexPage = () => (
<>
<SEO />
<p className="text-red-800">Hello world!</p>
</>
);
export default IndexPage;

Pour remplacer les balises meta par défaut, vous pouvez ajouter des propriétés au composant <SEO /> :

<SEO
title="title"
description="description"
image="https://example.com/image.png"
dateModified="2020-01-01T08:00:00.000Z"
datePublished="2020-01-01T08:00:00.000Z"
tags={["tag1", "tag2"]}
article={true}
/>

Références :