jahed.dev

Flattening Markdown into a single paragraph

To render Markdown, FrontierNav uses a combination of markdown-it to parse the Markdown into a plain object tree, and a custom React component to render that into React components. This lets FrontierNav enhance the result with interactive elements, like search links and video embeds, without needing to introduce more syntax into the parser. So a post like:

New game announced!

[](https://www.youtube.com/watch?v=wvunu2e29tc)

What do you think?

Would render as:

<p>New game announced!</p>

<div class="player">
  <iframe src="https://www.youtube.com/watch?v=wvunu2e29tc"></iframe>
</div>

<p>What do you think?</p>

Not all components want this however. For example, preview components just want to give a small glimpse of the post for visitors to click through to.

One thing I noticed a while ago was that these preview components weren't working as intended. They essentially took the raw Markdown and rendered the first 200 characters of it as-is. So a post like above would render as:

New game announced! [](https://www.youtube.com/watch?v=wvunu2e29tc) What do you think?

As in, you'd see the literal square brackets as plain text. Not great.

So how should I fix this? I just want to render the text. All of the popular Markdown renderers didn't provide this feature out of the box. I don't blame them, Markdown is made to render to HTML and providing a general solution covering all the edge cases seemed like a pain. So I sat on it and delayed a fix for months.

It's worth mentioning that I want to keep the post model lean and avoid introducing more concepts like asking users to add "titles" and "summaries" like a blog or forum might.

It took me a while to connect the dots and realise a simple solution: element.innerText. The only downside was I'd have to add a third step to the render pipeline for these previews, but it's fairly limited and cacheable.

const renderFlatMarkdown = (markdown: string): Promise<string> => {
  return new Promise<string>((resolve) => {
    const container = window.document.createElement("div");
    ReactDOM.render(<Markdown content={markdown} />, container, () => {
      const text = container.innerText.replace(/\s+/g, " ");
      resolve(text);
    });
  });
};

Here I'm defining a simple async function for components to use in their hooks or whatever else. It uses ReactDOM's render to render the custom Markdown component onto an off-screen element where it can extract the innerText and return it. The string renders like this:

New game announced!What do you think?

Note the lack of spacing between ! and What. To ensure paragraphs are spaced out, there's a replace call to swap out all forms of whitespace with a simple space; otherwise new lines will collapse into nothing when rendered onto the DOM. After this, the final result becomes:

New game announced! What do you think?

Success! Once I figured this out, I went on to use the same result in other places like window titles. Pretty useful.

Thanks for reading.