Using Dendron and Gatsby Together 🌲
đź“„đź“„

First published on .

Introduction

I love learning new tools and integrating them into my day to day life. Dendron is the latest in a long line of tools that I’ve used to keep track of the notes I use to remember things, jot down ideas, or think in a less polished way. Like several other tools I’ve used it’s local-first and based in markdown. What makes Dendron special to me is that currently it is the only digital tool I use for notes.

Since discovering the personal knowledge management space, and really diving into the space over the past year, I have used several different toolsBear, Notion, Roam, Org-Roam, Logseq, and Obsidian have all played a role in my stack at some point. but have always functionally divided — using one tool for my public notes that I host here on this website, and one tool where I wrote my private notes. What made this even more difficult is that often notes would begin their life in my private notes and then would need to be copied over to my public notes — an extra layer of friction that at first was no big deal but has become more and more tedious over time.

Dendron solves this problem and it does so, more or less out of the box, leveraging 11ty to create a really good looking static site that can be published wherever you want and with the ability to fine tune what notes are publishedHere are a couple examples of what this native solution does: garden.ianjones.us & kevincunningham.co.uk/garden. But I’ve spent a lot of time customizing my website and I didn’t want to lose the ability to have my notes fully integrated into my website.

If you’re interested in learning more about Dendron, check out their quickstart guide. The rest of this post is a short overview of how I integrated my Dendron notes with my Gatsby.js frontend.

The Setup

Dendron has several things which lend it to very easily integrate with Gatsby.js — the first and most important is that it stores all notes in Markdown with YAML frontmatter — exactly what Gatsby uses. Another thing that Dendron does that I really like, is it uses a static 32 digit long id so that even if the title or organizing structure changes, Dendron still knows how to find the note in question. I’ll used that id as the slug of my note (/note/<id>).

An example of Dendron Frontmatter

To add my Dendron folder to my website I first created a private github repository with my Dendron notes, then I added a private git submoduleTania Rascia has an excellent guide on how to set this up. to import the contents of that repository into my website.

To get gatsby-plugin-mdx to recognize those markdown files I added this to my existing graphql query in my gatsby-node.js file. Using the fileAbsolutePath to filter only notes in my Dendron folder.

dendron: allMdx(
        filter: {
          fileAbsolutePath: { regex: "/Dendron/" }
          frontmatter: { published: { eq: true } }
        }
      ) {
        edges {
          node {
            frontmatter {
              id
              title
            }
            id
          }
          previous {
            id
          }
          next {
            id
          }
        }
      }

Then I’ll pass the results of that query to the create pages call in that same file:

const dendron = result.data.dendron.edges;

dendron.forEach(({ node }) => {
  createPage({
    path: `notes/${node.frontmatter.id}`,
    component: path.resolve(`./src/components/notes/note.js`),
    context: { id: node.id },
  });
});

And then finally I’ll create the note.js file I used as a component up above:

import React from "react";
import { MDXRenderer } from "gatsby-plugin-mdx";
import Layout from "../layout/layout";
export default ({ data }) => {
  return (
    <Layout
      title={data.mdx.frontmatter.title}
      description={data.mdx.frontmatter.excerpt}
      type="Note đź“ť"
    >
      <MDXRenderer>{data.mdx.body}</MDXRenderer>
    </Layout>
  );
};

export const pageQuery = graphql`
  query NoteQuery($id: String) {
    mdx(id: { eq: $id }) {
      body
      excerpt
      frontmatter {
        title
        id
      }
    }
  }
`;

And just like that all of my Dendron notes are created on my website.

Ok so it’s not as simple as that…

One potential problem with using Dendron’s Markdown is that it uses wiki-links which Gatsby doesn’t support natively. Luckily there’s a plugin which suports wikilinks - gatsby-remark-doublebrackets-link. I’ll install that yarn add gatsby-remark-double-brackets-link and add it to my Gatsby config.

{
  "resolve": `gatsby-plugin-mdx`,
  "options": {
    "gatsbyRemarkPlugins": [
      {
        "resolve": `gatsby-remark-double-brackets-link`,
        "options": {
          "titleToURLPath": `${__dirname}/src/lib/dendron-parse-url.js`
        }
      }
    ]
  }
}

This transforms [[Link to page]] to [Link to page](titleToURL('Link to page')) and I can use a custom titleToURLPath function to remove the dendron alias syntax (which uses the | character to separate the alias from the wikilink). But I’m using the id defined in the frontmatter of the note and not the wikilink as the slug. How can I transform my wikilinks to [Link to page]('id from frontmatter of target link')?

I solved this problem by running a Static Query in my Layout component. This query fetches all of the relevant frontmatter from all of my dendron notes as well as the filename. I stored the relevant data (id, title, slug) in an array of objects with a key value of the filename.

const popups = {};
const posts = data.allMdx.nodes;
posts.map((post) => {
  if (post) {
    popups[`${post.slug.substring(post.slug.lastIndexOf("/") + 1)}`] = {
      title: post.frontmatter.title,
      body: post.body,
      slug: post.frontmatter.slug,
      dendronId: post.frontmatter.id,
      published: post.frontmatter.published,
    };
  }
});

I passed this array of note metadata to a custom mdx component using the custom components prop.I use this same technique to also pass in the body of my articles, poems, and notes to enable link previews. I then use custom regex to determine whether the markdown link is an internal link, an external link, or an internal notes link. If it’s an internal notes link I use the current href (which at this stage is still a wikilink) as the key in the array I passed in to see if there’s any Dendron notes that matches that href. If there is, I change how that markdown link is rendered and change the link to point from /notes/dendron.url to /notes/<dendron-id>.

My raw code for that operation is at the bottom of this article. You’ll see that I have to do some string manipulation to get it to work. I also include logic throughout to only allow access to notes that include published: true in the frontmatter. Any links to unpublished notes get redirected to a custom 404 page indicating that that note has note yet been published.

If you are considering using Dendron with your Gatsby site I hope that this has proved useful.

const AnchorTag = ({ href, popups, ...restProps }) => {
  const isInternalNotesLink = !isEmpty(href.match(INTERNAL_LINK_REGEX))
  const isInternalLink = !isEmpty(href.match(INTERNAL_NON_NOTES_LINK_REGEX))
  let renderedLink = restProps.children
  if (isString(restProps.children)) {
    renderedLink = restProps.children.replace(/\[\[(.*?)\]\]/g, '$1')
  }
  if (isInternalNotesLink) {
    if (renderedLink.includes('|')) {
      renderedLink = renderedLink.substring(0, renderedLink.lastIndexOf('|'))
    }
    if (isEmpty(popups[`${href.substring(href.lastIndexOf('/') + 1)}`])) {
      return (
        <InternalNotesLink
          to={`/notes/${href.substring(href.lastIndexOf('/') + 1)}`}>
          {renderedLink}
        </InternalNotesLink>
      )
    } else {
      //check if the note is published, if it's not go to a specific 404 page
      if (
        popups[`${href.substring(href.lastIndexOf('/') + 1)}`].published != true
      ) {
        return (
          <InternalNotesLink to="/notes/404">{renderedLink}</InternalNotesLink>
        )
      } else {
        return (
          <LinktipPreview
            link={true}
            tiptext={
              <MDXProvider components={components}>
                <h1>
                  {popups[`${href.substring(href.lastIndexOf('/') + 1)}`].title}
                </h1>
                <MDXRenderer>
                  {popups[`${href.substring(href.lastIndexOf('/') + 1)}`].body}
                </MDXRenderer>
              </MDXProvider>
            }
            placement="right"
            multiple={false}>
            <InternalNotesLink
              to={`/notes/${
                popups[`${href.substring(href.lastIndexOf('/') + 1)}`].dendronId
              }`}>
              {renderedLink}
            </InternalNotesLink>
          </LinktipPreview>
        )
      }
    }
  }

Author’s Note: I now use Obsidian exclusively to keep my notes, mainly due to its amazing mobile app; however I still use this same method to publish my notes and rely on Obsidian templates to generate the uuid to allow for publication.

Other things to read

  • Hypertext Writing
    Every medium has its strengths. There are considerable strengths inherent to a digital, web-based, way of thinking -- first and foremost is the sheer amount of interactivity and connections you can make.
  • How I Coded This Website
    A little bit about how I coded this website with GatsbyJS.
  • Create a Basic Svelte Site
    A short guide to creating a simple Svelte site with animations and transitions.

Contact

Content is copyrighted 2019-2023 © D.S. Chapman. This site was built using GatsbyJS. Code for this website is open source and available on Github