Blog

A crazy adventure with CORS, Nuxt, and Webmentions

The goal didn't sound daunting in and of itself: to incorporate the like portion of webmentions on my single blog post pages. I've seen them in various places and even found an article by Remy Sharp called Send Outgoing Webmentions that explains how to do the setup required for receiving webmentions along with supplemental links. I finished all the prerequisites, and I was able to see my webmentions after logging into Webmention.io.

Getting them on my site

Now there was the matter of implementation. There were a few options such as Javascript plugins webmention.js and embeds such as A Webmention Endpoint. Then there were APIs -- webmention.io even has its own API where I could retrieve a list of webmentions like https://webmention.io/api/mentions.jf2?target=https://webmention.io. That API gave me JSON, I could easily do something with that, lots of things.

Implementation with Nuxt

After I placed the necessary elements in the <head> and set up my Vue template to utilize the JSON, the next task was to get this JSON to my site. I was already using the asyncData method to get the JSON of my blog post (which come from physical files), so I tried to add getting the webmention JSON here too as follows:

async asyncData({ $axios, params, payload, route }) {
  const token = process.env.webmentionsToken;
  let likes = null;
  try {
    likes = await $axios.$get('https://webmention.io/api/mentions.jf2', {
      params: {
        target: 'https://jeremywynn.com' + route.fullPath + '/',
        token: process.env.webmentionsToken,
        'wm-property': 'like-of',
        'per_page': 20
      }
    });
  } catch(error) {
    console.log(error);
  }
  if (payload) {
    return { blogPost: payload, likes};
  }
  else {
    return {
      blogPost: await import(`~/assets/content/blog/${params.blog}.json`), likes
    };
  }
},

It worked!

Not so fast

I noticed that it was working whenever I loaded or refreshed the page, but the JSON wasn't loading when I clicked through my site. I looked at the Firefox Dev Tools Console and found this error:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://webmention.io/api/mentions.jf2?target=https:%2F%2Fje…token=blahblahblahblah&wm-property=like-of&per_page=20. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

Chrome, Internet Explorer, and Opera seemed fine with it though. It was Firefox and Safari that had this problem involving CORS. I noticed in the Firefox Network panel that the request was being made with the OPTIONS method while Chrome always used GET. The OPTIONS method is how a preflight request is made, but it seemed that the API server of webmention.io does not include the necessary elements in its Preflight Response that Firefox or Safari wanted (like Access-Control-Allow-Origin).

I needed application/json, and there was no way to make this a simple HTTP request that would make Firefox/Safari not use the OPTIONS method.

I thought I knew what to do

The sort of weird behavior was occuring because asyncData is called server-side once (on the first request to the Nuxt app) and client-side when navigating to further routes (webmentions were showing up in Firefox/Safari when I manually refreshed the page). I knew that the @nuxtjs/proxy can be used to make external requests look like it came from your own site.

I had this. So I made this update to nuxt.config.js:

modules: [
  '@nuxtjs/axios',
  '@nuxtjs/proxy'
],
proxy: {
  '/api/mentions.jf2': {
    target: 'https://webmention.io'
  }
},

After updating my axios call under the asyncData area in my component to remove the https://webmention.io/ portion, I clicked around my site and everything was working everywhere! Gleefully, I pushed all the updates (after a lot of research and work) to Netlify, but then I noticed this critical @nuxtjs/proxy caveat:

⚠ Does not work in generated/static mode!

&#*#^%&#!

What else could be done

I did not have control over the webmention.io API server. The CORS problem is not the fault of axios. Passing the axios call whatever configuration options I found in desperation did nothing. There were at least a few other options:

  1. Use JSONP: The webmention.io API does support it with the inclusion of the jsonp parameter. It has been used to bypass cross-origin sharing pain in the past.
  2. Use another API such as A Webmention Endpoint. Maybe this server would handle the OPTIONS method from requests differently.
  3. Use Javascript or HTML embed methods mentioned earlier
  4. Use and host my own instance of CORS Anywhere
  5. Wait for something like Warpist
  6. Cry?

Why can't this shit just work?

The answer: Middleware

I am not entirely sure how right now, but using middleware works since it makes the requests to the webmention.io API always utilize GET even in Firefox/Safari.

In middleware/webmention.js:

import axios from 'axios'

export default async function ({ route, store }) {
  const likes = await axios.get('https://webmention.io/api/mentions.jf2', {
    params: {
      target: 'https://jeremywynn.com' + route.fullPath + '/',
      token: process.env.webmentionsToken,
      'wm-property': 'like-of',
      'per_page': 20
    }
  });
  store.dispatch('setWebMentions', likes.data);
}

Vuex store is how I am delivering this webmention JSON for my site. In store/index.js:

export const state = () => ({
  webmentions: null
});

export const mutations = {
  SET_WEB_MENTIONS(state, webmentions) {
    state.webmentions = webmentions;
  }
};

export const actions = {
  setWebMentions({ commit }, webmentions) {
    commit('SET_WEB_MENTIONS', webmentions);
  }
};

In my page component .vue file:

Things will be handled by the middleware now, so I removed the axios and likes related code from asyncData. I added a computed entry for likes to get them from the store:

computed: {
  likes() {
    return this.$store.state.webmentions;
  }
},

and made sure to call the middleware in the component:

middleware: 'webmention',

Now I can click around and have webmentions load correctly without any CORS issues.