I created a bookmarklet to create the table of content for forum posts

Table of contents:

I created a bookmarklet to generate a foldable table of contents (ToC) like the one shown above.

I hope it helps avid community members who write long texts!

Summary

I sometimes write long topics/posts and needed a ToC for easier reading.
I found some existing work, such as DiscoTOC - automatic table of contents, but I needed a tool just for me, not needing to install something across the community.

After publishing a structured post, click the bookmarklet, and the ToC will be copied to your clipboard. Edit the post and paste the ToC at the top!

How to use

Installation

  1. Save a page as a bookmark.
  2. Edit the name, such as “:clipboard: Copy forum ToC to clipboard.”
  3. Edit the URL and paste the following code. Customize the code if needed – see two “Optional” items below.
javascript:(function() {
	const copyForumTocToClipboard = function() {
		const urlMatch = window.location.href.match(/\/t\/[^\/]*\/\d+\/?(\d*)/);
		if (!urlMatch) return;

		const postIndex = urlMatch ? urlMatch[1] : 1;
		const anchors = document.querySelectorAll('#post_' + postIndex + ' div.cooked h6>a.anchor,h5>a.anchor,h4>a.anchor,h3>a.anchor,h2>a.anchor,h1>a.anchor');
		if (!anchors) return;

		let toc = '';
		anchors.forEach(anchor => {
			toc +=
				' '.repeat((anchor.parentNode.nodeName[1] - 1) * 4) +
				`<a href="${anchor.href}">${anchor.parentNode.textContent}</a><br>\n`;
		});
		if (!toc) return;

		navigator.clipboard.writeText('<details open><summary>Table of contents: </summary><ul>\n' + toc + '</ul></details>');
	};

	copyForumTocToClipboard();
})();

What the code does

  1. Check if the URL seems to be a community post: https://{domain}/t/{title}/{topicID}(/{postIndex}).
  2. Check if :link: anchors (headings such as <h1> and # ) are included in the post.
  3. Generate HTML code for ToC.
  4. Copy the code to the clipboard.

Generating ToC

  1. Publish a post with structure (HTML <h1>, <h2>, … and Markdown # , ## , …). Discourse will assign anchors to each heading.
  2. Make sure your post is selected by looking at the progress bar (e.g., 1/22) or the URL (e.g., /1).
  3. Click the bookmarklet in the bookmark bar.
  4. The ToC is copied to your clipboard.

Using ToC

  1. Click the pencil icon to edit the post.
  2. Paste the code at the top.
  3. Check if the ToC items are correctly displayed (Known issue: the bookmarklet misses some emojis).
  4. (Optional) Change/translate “Table of contents”.
  5. (Optional) Delete open if you want to collapse the ToC by default.
  6. Click “Save Edit.” If you do this in a few minutes after publishing the post, the “edited” pencil icon is not added to your post.

Technical notes for technical people

  • You can also copy and paste the JavaScript code into the developer console.
    • I wrapped the code in a function because return; doesn’t work when used outside of functions.
  • The '&nbsp;'.repeat() approach looks messy when previewing the topic, but it looks the best in the actual post (compared with using <li></li>) in my opinion.
  • When I tested querySelectorAll, somehow the first item in h6>a.anchor,h5>a.anchor, ... was not found. I put h6 at the beginning because that’s probably used least frequently.
  • The bookmarklet might stop working if Discourse changes its UI/DOM. Please reply to me if you find errors.

Screenshots

As a new user in meta.discourse.org, I cannot add multiple images, so I’m batching all screenshots in one image:

18 Likes

Ohh nice - could that be extended to create a TOC for the entire post with all replies?

3 Likes

Posts generally load about 20 at a time, I guess it would be difficult to create a TOC of all posts at once

4 Likes

Very cool! Thanks for sharing @ShunS!

I love how you framed the problem you’re solving with this tool, and how it is designed to meet the needs of important users on sites who themselves may not have permissions to change things on the site itself as things exist today.

10 Likes

Thank you for the replies :slight_smile:

As @Lhc_fl says, the DOM appreach (HTML/JavaScript) is limited in the number of items we can manage. Discourse API might help for such actions.
Another problem with creating TOC for replies is that it’s not easy to define what would be the heading title for each reply. We could use information like the author and the date, but I’m not sure how it’s more useful than the existing scrolling progress bar.

Thank you @mcwumbly!

6 Likes

@ShunS this is very cool, thank you for sharing. also, welcome to Meta :wave: :slight_smile:

5 Likes

This fabulous and exactly what I was looking for. I seems to solve this issue

Why not submit this to Discourse to include it as an official plug-in or feature? It could be included in the composer/editor toolbar to automatically insert a TOC in the post.

6 Likes

I have one suggestion, is it possible to add a bullet at the beginning TOC line? In my case each heading is a long line, so the bullet helps differentiate one entry from the next

1 Like

Hi @RBoy, thank you for the feedback and the suggestion!

An editor plugin would be great, but it’d be a lot of effort to read the source code of discourse to understand the logic of handling emojis and defining the heading/anchor text and create a repository of the plugin.

A (seemingly) simple plugin like Spoiler Alert is a big repo and I don’t have a bandwidth to fully commit to the development. So, I hope Discourse can prioritize feature requests like Automatic Table of Contents generation and develop an native feature in the meantime :pray:


Below is the version with the bullet points. The spaces between <ul> and <ul> are rather too large, so I preferred the original unbulleted version.

Screenshot:

Code:

javascript:(function() {
	const copyForumTocToClipboard = function() {
		const urlMatch = window.location.href.match(/\/t\/[^\/]*\/\d+\/?(\d*)/);
		if (!urlMatch) return;

		const postIndex = 1;
		const anchors = document.querySelectorAll('#post_' + postIndex + ' div.cooked h6>a.anchor,h5>a.anchor,h4>a.anchor,h3>a.anchor,h2>a.anchor,h1>a.anchor');

		let toc = '';
		let currentLevel = 1;
		anchors.forEach(anchor => {
			newLevel = anchor.parentNode.nodeName[1];
			levelChange = newLevel - currentLevel;
			toc +=
				((levelChange >= 0) ? '<ul>'.repeat(levelChange) : '</ul>'.repeat(levelChange * -1)) +
				`<li><a href="${anchor.href}">${anchor.parentNode.textContent}</a></li>`;
			currentLevel = newLevel;
		});
		if (newLevel > 1) toc += '</ul>'.repeat(newLevel - 1);
		toc = '<details open><summary>Table of contents: </summary><ul>\n' + toc + '</ul></details>';

		navigator.clipboard.writeText(toc);
	};

	copyForumTocToClipboard();
})();
5 Likes

We already have a ToC plugin and it doesn’t need to maintain the ToC in the post body.

I’d definitely recommend that one if installed as it can’t go out of sync.

This tool looks great for when DiscoToC is not available though. Nice work! :+1:

4 Likes

This can be done using a theme component. You can take a look at the decorateCookedElement method of the api, it should be useful.

4 Likes

HI @supermathie, thank you for the reply.

when DiscoToC is not available

Yes, that’s the differentiation. I created the bookmarklet because I’m not in a position to decide which “theme-component” to install to the forum I’m usually in, and only a few people write long texts that need a TOC.


Thank you @Lhc_fl, that’s very helpful!

I searched the GitHub repo and found that method. I’ll consider developing that (only) when I have good bandwidth and I see plenty of demand for the feature.

However, if a theme-component can be added to the community, there’s already DiscoTOC as @supermathie said :slight_smile:

4 Likes

Unfortunately that plug-in totally unusable when the heading more then a few words long as it make a complete mess of things. If the headers are one or two lines long (for example a FAQ page) then the Disco TOC plug-in makes a complete mess of the page which is why I had raised this request for an inline TOC (which this one provides) and is perfect for such pages

With the amount of genius/talent amongst the teams who build discourse it shouldn’t be that hard to include this amazing feature as an alternative to DiscoTOC to give it a much wider range of use.