frontmatter-markdown-loader: "[WARN] Cannot stringify a function render" after upgrade to 3.x

I’ve been building a static blog with Nuxt.js, and I’m using frontmatter-markdown-loader with [Mode.VUE_RENDER_FUNCTIONS] to render Markdown pages and posts which contain Vue components. This was working great on v2.3.0, but after upgrading to v3.1.0, I cannot properly render Markdown files which are loaded dynamically using Nuxt’s asyncData function.

Here’s my dynamic render component:

<!-- components/DynamicMarkdown.vue -->

<script>
export default {
  props: {
    renderFn: {
      type: Function,
      required: true,
    },
    staticRenderFns: {
      type: Array,
      required: true,
    },
  },
  data() {
    return {
      templateRender: null,
    };
  },
  created() {
    this.templateRender = this.renderFn;
    this.$options.staticRenderFns = this.staticRenderFns;
  },

  render(createElement) {
    return this.templateRender ? this.templateRender() : createElement('div', 'Rendering...');
  },
};
</script>

And here is the page component for individual blog posts:

<!-- pages/blog/_slug.vue -->

<template>
  <DynamicMarkdown
    :render-fn="renderFn"
    :static-render-fns="staticRenderFns"
  />
</template>

<script>
import DynamicMarkdown from '~/components/DynamicMarkdown.vue';

export default {
  components: {
    DynamicMarkdown,
  },
  async asyncData({ params }) {
    const article = await import(`~/content/articles/${params.slug}.md`);
    return {
      renderFn: article.vue.render,
      staticRenderFns: article.vue.staticRenderFns,
    };
  },
};
</script>

This works if I link to a blog post from somewhere else in the app (ie, the post is rendered client-side). However, if I reload the page, or if I visit the permalink directly, the page crashes and I see several errors. In the browser console, I get TypeError: this.templateRender is not a function.

Screenshot 2019-11-09 12 37 26

And in the terminal I see two warnings: WARN Cannot stringify a function render and WARN Cannot stringify a function.

Screenshot 2019-11-09 12 37 52

With FML version 2.3.0, this approach worked fine, both with client-side and server-side rendering.

Another releveant bit of information is that if I first load a Markdown file at the top of my <script> section using regular ES6 module syntax, everything works fine.

The following code allows the page to be loaded either client-side or server-side:

<!-- pages/blog/_slug.vue -->

<template>
  <DynamicMarkdown
    :render-fn="renderFn"
    :static-render-fns="staticRenderFns"
  />
</template>

<script>
import DynamicMarkdown from '~/components/DynamicMarkdown.vue';
import article from '~/content/articles/2019-10-14-my-post.md';

export default {
  components: {
    DynamicMarkdown,
  },
  data() {
    return {
      fm: null,
      renderFn: null,
      staticRenderFns: null,
    };
  },
  created() {
    this.fm = article.attributes;
    this.renderFn = article.vue.render;
    this.staticRenderFns = article.vue.staticRenderFns;
  },
};
</script>

Obviously, the previous code example is not practical since blog posts must be loaded dynamically by extracting the file name from params. Hence the need for asyncData imports.

In summary, if I import a Markdown file using ES6 module syntax, everything works. But if I import it inside asyncData it breaks.

If it helps to see a complete app that demonstrates this issue, please have a look at nuxt-markdown-blog-starter by @marinaaisa. I referenced her code a lot when building my own blog (thank you, @marinaaisa!), and after she recently upgraded FML to v3.0.0, her app manifests the exact problem I have described above.

I am aware that FML v3.0.0 introduced breaking changes, and as best I can tell the root issue is that vue.render and vue.staticRenderFns now return functions instead of strings. I’ve looked at your source code to try and find a workaround, but I’m afraid my understanding of Vue render functions is too rudimentary.

Thank you for all your work on frontmatter-markdown-loader. I really love this project since it enables Vue components to be embedded in Markdown files. This is a huge win for blogging, and I really hope a solution can be found to allow for asyncData file imports. I would appreciate any help or advice you can offer on this issue!

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 3
  • Comments: 24 (10 by maintainers)

Commits related to this issue

Most upvoted comments

@lukeocodes Right. Unfortunately, Vue’s async component feature only handles component importing for SSR.

so,

created () {
  this.selectedArticle = () => {
    return import(`~/articles/${this.$route.query.name}.md`).then(({ vue, attributes }) => {
      this.attributes = attributes
      return vue.component
    })
  }
}

doesn’t work for attributes in the server-side. The below works for SSR instead, but lose benefits of async component 😞

created () {
  const markdown = require(`~/articles/${this.$route.params.slug}.md`)
  this.attributes = markdown.attributes
  this.markdownContent = markdown.vue.component
}

Nuxt team recently released “content module”, that may solve all markdown handling in Nuxt without this loader 😉 https://github.com/nuxt/content

Hmmm, I don’t think we need to persist in using asyncData because asyncData is designed for passing primitives which can be generated only in backend servers to use in clients. And typical FML origin components can work with both. can’t imagine the exact use-case we need to use asyncData yet.

beforeCreate and created hooks are called in SSR process. (Try cURL your Nuxt server, you can see imported components on HTML before running JS in the frontend, Joshua) https://ssr.vuejs.org/guide/universal.html#component-lifecycle-hooks So, pre-rendering/SSR could work without using asyncData.

Eventually, I prefer to remove exporting render / staticRenderFns because that is exposing Vue component’s internal processing which end-user don’t need to care basically. I regret I exposed that way originally, but really encourage users to use vue.component instead 😉

Even so, you love to use asyncData, stick version of FML as 2.x or serialize imported function with JSON.stringify, toString() or whatever.

@hmsk

This might be a lot to ask, however it would be amazing if we could actually load the vue component inside of nuxt asyncData. Loading the component in asyncData makes it load server side, which slightly increases the performance, but most importantly it allows the page to be loaded without javascript. Not quite sure how that could be implemented though, perhaps a custom serializer?

@joshukraine thanks for waiting for my late second reply 🙃 The code block is just pasted from my project (TypeScript + property-decorator), so that might confuse you. Below is the minimum code of SFC to render markdown and attributes.

<template>
  <div>
    <h1>{{ attributes.title }}</h1>
    <component :is="markdownContent" />
  </div>
</template>

<script>
export {
  props: {
    slug: String
  },
  data () {
    return {
      markdownContent: null,
      attributes: null
    }
  },
  created () {
  this.markdownContent = () => import(`~/static/episodes/${this.slug}.md`).then((md) => {
    this.attributes = md.attributes;
    return {
      extends: md.vue.component
    };
  }
}
</script>

Ref (The repo is the original use-case of frontmatter-markdown-loader before I founded): https://github.com/haiiro-io/violet/blob/master/components/Work/DynamicMarkdown.vue

@joshukraine , From your example, I think you’re using Nuxt Markdown Blog starter from @marinaaisa. I’ve created PR to solve this issue and it already merged as well, https://github.com/marinaaisa/nuxt-markdown-blog-starter/pull/9

You’re not using Async Components correctly. https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components

diff --git a/pages/index.vue b/pages/index.vue
index 7637a37..7baa8ad 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -8,8 +8,10 @@
   export default {
     data: () => ({component: null}),
     async created() {
-      const file = await import('~/README.md');
-      this.component = file.vue.component
+      this.component = async () => {
+        const file = await import('~/README.md');
+        return file.vue.component;
+      }
     }
   }
 </script>

The original code seems to work for the client side in coincidence unfortunately 🙃

@hmsk

I guess the title requires null-check since the attributes are provided asynchronously

Aha, of course! That worked. 🙂 As far as I’m concerned, that resolves this issue, so I’ll go ahead and close.

Thanks again, and have an awesome day! 😎🙌🏻

@joshukraine

FWIW, you have a couple of typos in that code.

Haha, that might be. I didn’t run that code actually 🙈

But if I reference it anywhere in the view template, the whole page crashes with a TypeError: Cannot read property ‘title’ of null.

I guess the title requires null-check since the attributes are provided asynchronously 🤔

<p v-if="attributes">Post title: {{ attributes.title }}</p>

And thanks for giving me coffees on https://www.buymeacoffee.com/hmsk 🙏 I really appreciate and am honored.

@hmsk Thank you so much for your reply and the example code! That helped tremendously. I’ve implemented your solution in my blog and for the most part it works as you demonstrate in your code sample. (FWIW, you have a couple of typos in that code.)

Anyway, the one thing that does not seem to work as intended is the frontmatter. The attributes data property loads fine and is visible in Vue devtools.

Screenshot 2019-11-21 17 59 28

But if I reference it anywhere in the view template, the whole page crashes with a TypeError: Cannot read property 'title' of null.

<!-- components/DynamicMarkdown.vue -->
<template>
  <div>
    <p>Post title: {{ attributes.title }}</p>
    <component :is="markdownContent" />
  </div>
</template>

Screenshot 2019-11-21 17 57 07

For my use case it’s actually not a big deal since I am loading the frontmatter in a different way. But I think for many people it would be an issue.

If it helps, my Nuxt blog is now complete and I’ve open sourced the repo. The FML 3.x upgrade is in this PR: https://github.com/joshukraine/ofreport.com/pull/44

Thank you again for your help! 😃

Of course. Here’s the applyHighlight method I made:

    applyHighlight(node, h)
    {
        if (this.highlight.length>0 && node)
        {
            if (node.text)
            {
                if (node.text.length >= this.highlight.length)
                {
                    let result = [];
                    let i = 0;
                    let textSoFar = '';
                    while (i < node.text.length)
                    {
                        if (node.text.substring(
                            i,
                            i+this.highlight.length
                        )==this.highlight)
                        {
                            result.push(this._v(String(textSoFar)));
                            result.push(h('span', {
                                class: 'highlight'
                            }, this.highlight));
                            textSoFar = '';
                            i += this.highlight.length;
                        }
                        else
                        {
                            textSoFar += node.text.substring(i, i+1);
                            i++;
                        }
                    }
                    if (textSoFar.length>0)
                    {
                        result.push(this._v(String(textSoFar)));
                    }
                    return result;
                }
            }

            if (node.children && node.children.length>0)
            {
                let children = [];

                node.children.forEach(child =>
                {
                    const result = this.applyHighlight(child, h);
                    if (Array.isArray(result))
                    {
                        children = children.concat(result);
                    }
                    else
                    {
                        children.push(result);
                    }
                });
                node.children = children;
            }
        }
        return node;
    }
},

node is the resulting VNode, h is the function to create them. I then just

render (createElement)
{
    return this.templateRender ? this.applyHighlight(this.templateRender(),
                                                     createElement) :
        createElement('div', 'Rendering...');
}

Where would I search for the highlighted text with Mode.VUE_COMPONENT? md.vue.component.$el?

btw component mode might be easier to use. Here’s my recent dynamic-markdown-ish implement with Nuxt. (And I’m planning to remove render / staticRenderFns at all since component may cover every cases)

<client-only>
  <component :is="episodeContent" />
</client-only>
episodeContent: Function | null = null;
attributes: any = {};
@Prop({ type: String, required: true }) slug!: string;
@Prop({ type: Boolean, default: false }) expand!: boolean;

created () {
  this.episodeContent = () => import(`~/static/episodes/${this.slug}.md`).then((c) => {
    this.attributes = c.attributes;
    return {
      extends: c.vue.component
    };
  });
}

That seems not this loader’s problem.

Since asyncData in Nuxt doesn’t expect function in a returning object. On SSR process (so when reloading the page as you mentioned), Nuxt does some converting for the returned object. Then that shows such warnings.

image image

Not sure why nuxt-markdown-blog-starter uses asyncData for markdown content functions. Because the imported contents are rendered in under <client-only> 🤔 Why don’t you use created () ?

I’d like to think about supporting “stringify-ed exports” as well as previous versions if there is a reasonable use-case, but I’m not convinced by the approach of nuxt-markdown-blog-starter.