Feather Wiki

A tiny tool for simple, self-contained wikis!

Extension Development /

Tutorial

Created:

Extending Feather Wiki is a little bit like a puzzle. So long as you have enough JavaScript knowledge and a little bit of insight into how Feather Wiki functions, you should be able to alter nearly anything about Feather Wiki!

This tutorial will walk you through the process of creating an extension that adds a clickable image next to the wiki title. For this tutorial, we'll use the Feather Wiki logo so I don't have to upload a new image: Feather Wiki icon

Step 1: Boilerplate #

While it's not strictly necessary to use, I have found that the following scaffolding structure is very reliable for writing Feather Wiki extensions:

FW.ready(() => { // Wait until Feather Wiki is fully initialized
  const { state, emitter } = FW; // Extract state & emitter for easier use
  console.log('running extensionName'); // Indicate that the extension has been added
  /*
    If you need to add variables to the Feather Wiki state, you can do so here
  */
  // Make the extension run *both* when the page renders *and* when the page first loads
  ['DOMContentLoaded', 'render'].forEach(ev => {
    emitter.on(ev, () => {
      setTimeout(() => { // Adds a very small delay so it injects after render when elements exist in DOM
        renderYourExtension();
      }, 50);
    });
  });
  emitter.emit('DOMContentLoaded'); // Ensure your extension renders

  function renderYourExtension () {
    // Your Extension Code
  }
});

This bit of code allows you to reliably 1) load the extension as soon as the Feather Wiki core is available, 2) render immediately, and 3) render every time a page is loaded. Again, you don't have to use it, but I've used it in every single extension I've written so far.

Step 2: Find the Parent #

Now that you have your boilerplate, you need to examine how Feather Wiki's HTML is structured when it renders. Our goal is to find the DOM query selector that gets only the element we want to insert our image into. We're looking for the element that contains the wiki's title, so we right click the title and open the inspector to see:

...
<span class="db">
  <a href="/Users/robbie/Projects/FeatherWiki/index.html" class="t">Feather Wiki</a>
</span>
...

Right off the bat, we can see that this is an a tag with a class of t inside of a span tag with a class of db. Looking at the styles, we can see that the t class provides the following styling:

font-size: 1.5rem;
font-weight: 700;
margin: 0.5rem 0;

So that's why the font is large and bold.

We want our image to be clickable separately from linking to the wiki's home page, so we want to put it inside of the a tag's parent element, the span tag with the db class. If we look at the styles for the db class, we can infer that it is simply a utility class for specifying display: block;, which means that we can't just target elements with a class of db since it is likely used elsewhere in Feather Wiki.

So we have to zoom out a little bit further:

...
<main>
  <div class="sb">
    <span class="db">
      <a href="/Users/robbie/Projects/FeatherWiki/index.html" class="t">Feather Wiki</a>
    </span>
    ...
  </div>
  ...
</main>
...

Using this, we can target the exact span element we want with document.querySelector('main > .sb > span.db')

Step 3: Create the Element #

Now that we know what element to create, we can start writing HTML that we want to inject. If you only need to inject static HTML, then you can just utilize plain strings in combination with Element.innerHTML, but we want our image element to be interacted with!

Luckily, Choo's nanohtml comes to our rescue by allowing us to not only write regular HTML as a string, but also to insert JavaScript event code directly into the string! So we can create our element ridiculously easily like so:

const clickableImage = html`<img src="./favicon.ico" onclick=${() => {
  alert('You clicked the logo!');
}} alt="Feather Wiki Logo" />`;

And that's our HTML element that we're going to insert.

Step 4: Insert the Element #

Now that we have both the element and the parent to add it to, we can simply combine them like so:

const clickableImage = html`<img src="./favicon.ico" onclick=${() => {
  alert('You clicked the logo!');
}} alt="Feather Wiki Logo" />`;
document.querySelector('main > .sb > span.db').appendChild(clickableImage);

And voilà, we have our image injected after the title!

Combine this with the boilerplate, and you get the image reliably added every time a page is rendered:

FW.ready(() => {
  const { state, emitter } = FW;
  console.log('running addLogoToTitle Extension');
  ['DOMContentLoaded', 'render'].forEach(ev => {
    emitter.on(ev, () => {
      setTimeout(() => {
        addLogoToTitle();
      }, 50);
    });
  });
  emitter.emit('DOMContentLoaded'); // Ensure your extension renders

  function addLogoToTitle () {
    const clickableImage = html`<img src="./favicon.ico" onclick=${() => {
      alert('You clicked the logo!');
    }} alt="Feather Wiki Logo" />`;
    document.querySelector('main > .sb > span.db').appendChild(clickableImage);
  }
});

Notice the order that things are happening here. The addLogoToTitle code is being run 50 milliseconds after the content is rendered. All extensions are really doing is modifying the window's DOM once the Feather Wiki content has finished rendering to the DOM! So if you can figure out how you need to modify the DOM, all you need to do is run that code after the base document finishes rendering.

Step 5: Tidying Up #

We could stop at the end of step 4, but to prevent very possible double rendering from happening, we can add one more line of code before declaring the clickableImage variable in the addLogoToTitle function to ensure that it only adds the logo to the title once:

if (document.querySelector('main > .sb > span.db img')) return;

This checks to see whether there is an img tag inside of the span tag we are targeting, and if there is, then stop processing the function so it doesn't render again.

Putting it all together, the final result is:

FW.ready(() => {
  const { state, emitter } = FW;
  console.log('running addLogoToTitle Extension');
  ['DOMContentLoaded', 'render'].forEach(ev => {
    emitter.on(ev, () => {
      setTimeout(() => {
        addLogoToTitle();
      }, 50);
    });
  });
  emitter.emit('DOMContentLoaded'); // Ensure your extension renders

  function addLogoToTitle () {
    if (document.querySelector('main > .sb > span.db img')) return;
    const clickableImage = html`<img src="./favicon.ico" onclick=${() => {
      alert('You clicked the logo!');
    }} alt="Feather Wiki Logo" />`;
    document.querySelector('main > .sb > span.db').appendChild(clickableImage);
  }
});

Suppose we only want it to display on a particular page. We can simply alter the if statement to check if we're on the wrong page. For example to only display when we're on the Wiki Settings page, you can change it to:

if (state.query.page !== 's' || document.querySelector('main > .sb > span.db img')) return;

The FW.state.query object contains any URI search components that exist in the URL, which is typically just page, i.e. the slug of the current page.

Conclusion #

This example is extremely rudimentary and doesn't result in anything particularly useful, but it really does show everything you need to know in order to write your own extensions! You can definitely write more robust extensions by leveraging internal Feather Wiki functions, but that will require you to make yourself more familiar with Feather Wiki's internal structure. The Documentation is good for that, but sometimes you might simply need to dive into the Feather Wiki source code.

It might also help to look at the source code for the official Feather Wiki Extensions to get an idea of what else you can do with Feather Wiki in combination with other code. Combining custom JavaScript with custom CSS can result in some very creative results if you just put your mind to it!