SSR and Vuex meta data



  • Hi guys,
    I’m so frustrated, i can’t find any working solution that https://metatags.io/ accepts.

    I want to:

    1. load a post page
    2. get post data from API using slug in Vuex
    3. use the post title from Vuex in my page meta

    I have followed the documentation with the example “Some title” and this changes on page load to “Another title” but in https://metatags.io/ i get “Some title”.

    Please point me in the right direction, is there a complete working example anywere?

    Step 1 and 2 is working great but step 3 accessing the Vuex data to use in meta title won’t work. I just get undefined.

    Here are my codes:

    post.vue

    async preFetch ({ store, currentRoute, previousRoute, redirect, ssrContext }) {
        if (!ssrContext) {
          const payload = { post_slug: currentRoute.params.post }
          return await store.dispatch('posts/setPostObj', payload)
        }
      },
      computed: {
        ...mapState('posts', ['postObj', 'postMeta']),
        ...mapGetters('posts', ['getPostMeta']),
        ...mapGetters('settings', ['getLocale'])
      },
      mounted () {
        if (process.browser) {
          this.doSetPostObj()
        }
      },
      methods: {
        async doSetPostObj () {
          try {
            const payload = { post_slug: this.$route.params.post }
            return await this.$store.dispatch('posts/setPostObj', payload).then(() => {
              document.title = `${this.postMeta.title} - ${this.$t('general.appName')}`
            })
          } catch (error) {
            this.$router.replace('/error')
          }
        }
      },
      meta () {
        return {
          title: this.getPostMeta.title,
          meta: {
            title: {
              name: 'title',
              content: this.getPostMeta.title
            },
            description: {
              name: 'description',
              content: this.getPostMeta.description
            },
            ogType: {
              property: 'og:type',
              content: 'website'
            },
            ogUrl: {
              property: 'og:url',
              content: this.getPostMeta.ogUrl
            },
            ogTitle: {
              property: 'og:title',
              content: this.getPostMeta.title
            },
            ogDesc: {
              property: 'og:description',
              content: this.getPostMeta.description
            },
            ogImage: {
              property: 'og:image',
              content: this.getPostMeta.ogImage
            },
            twitterTitle: {
              name: 'twitter:title',
              content: this.getPostMeta.title
            },
            twitterDesc: {
              name: 'twitter:description',
              content: this.getPostMeta.description
            }
          }
        }
    

    Vuex action

    export const setPostObj = async function (context, payload) {
      return axiosInstance.get('api/site/getPostObj', {
        params: { post_slug: payload.post_slug }
      }).then(({ data }) => {
        context.commit('setPostObj', data)
        context.commit('setPostMeta', data)
      })
    }
    


  • @PeterQF Try changing your component code to this:

      async preFetch ({ store, currentRoute, previousRoute, redirect, ssrContext }) {
        try {
          const payload = { post_slug: currentRoute.params.post }
          await store.dispatch('posts/setPostObj', payload)
        } catch(error) {
          redirect('/error')
        }
      },
      computed: {
        ...mapState('posts', ['postObj', 'postMeta']),
        ...mapGetters('posts', ['getPostMeta']),
        ...mapGetters('settings', ['getLocale'])
      },
      meta () {
        return {
          title: this.getPostMeta.title,
          meta: {
            title: {
              name: 'title',
              content: this.getPostMeta.title
            },
            description: {
              name: 'description',
              content: this.getPostMeta.description
            },
            ogType: {
              property: 'og:type',
              content: 'website'
            },
            ogUrl: {
              property: 'og:url',
              content: this.getPostMeta.ogUrl
            },
            ogTitle: {
              property: 'og:title',
              content: this.getPostMeta.title
            },
            ogDesc: {
              property: 'og:description',
              content: this.getPostMeta.description
            },
            ogImage: {
              property: 'og:image',
              content: this.getPostMeta.ogImage
            },
            twitterTitle: {
              name: 'twitter:title',
              content: this.getPostMeta.title
            },
            twitterDesc: {
              name: 'twitter:description',
              content: this.getPostMeta.description
            }
          }
        }
    
    

    Edit: From looking at the code, your component actually never called the posts/setPostObj action on the server, because in preFetch you have if(!ssrContext) and the mounted hook doesn’t run on server side with SSR (only beforeCreate and created.) The server must have the post object to set the meta tags, else it won’t be picked up by metatags.io



  • @beets thank you for answering. Any idea how i can solve this problem with the server issue?



  • @PeterQF Did you try the code I posted? It should work to fix the meta tag issue.



  • @beets , yes i have and i get:

    response: undefined,
    isAxiosError: true,

    when i skip:

    if (!ssrContext) {…



  • @PeterQF What if you try to use the full URL here:

      return axiosInstance.get('api/site/getPostObj', {
    

    i.e. https://example.com/api/site/getPostObj



  • @beets i need to include API key in header so this is my code in boot/axios.js:
    ´´´
    const axiosInstance = axios.create({
    baseURL: process.env.PROD
    ? ‘https://api.lsvab.se
    : ‘http://api-lsvab.lndo.site’,
    timeout: 180000,
    headers: {
    ‘X-Client-Api-Key’: ‘XXXXXXXXXXXXXXXXXXXXXXXXXX’,
    ‘Content-Type’: ‘application/json’
    }
    })
    ´´´



  • @PeterQF I see. What I can say for sure is that you don’t want if (!ssrContext) { in your prefetch, or the meta tags won’t work. We need to figure out why axios gives an error, can you provide the full output (from the terminal window running quasar dev) if you put a console.log here:

      async preFetch ({ store, currentRoute, previousRoute, redirect, ssrContext }) {
        try {
          const payload = { post_slug: currentRoute.params.post }
          await store.dispatch('posts/setPostObj', payload)
        } catch(error) {
          console.log(error)
          redirect('/error')
        }
      },
    


  • @PeterQF Also, do you control that API endpoint? Is it possibly checking for a hostname to equal some value? If so, you may need to set the host header:

    const axiosInstance = axios.create({
    baseURL: process.env.PROD
    ? 'https://api.lsvab.se’'
    : 'http://api-lsvab.lndo.site',
    timeout: 180000,
    headers: {
    'X-Client-Api-Key': 'XXXXXXXXXXXXXXXXXXXXXXXXXX',
    'Content-Type': 'application/json',
    'Host': 'frontendurl.com'
    }
    })
    

    Of course, setting Host only can be done in the server process. I don’t think it will affect anything being there on the client, but if so, you’d just have to check which environment you’re in and conditionally add it. I.e if(process.env.SERVER)



  • @beets ok so if i understand you correct preFetch is to be used on both client and server page load?

    Adding Host: ‘api-lsvab.lndo.site’

    gives me:

    Refused to set unsafe header “Host”



  • @beets WOW, major breakthrough.

    Turned out that my local env had a problem with Axios calls in preFetch due to Lando/Docker.

    Removing

    if (!ssrContext) {

    works great on production server and https://metatags.io/ shows all meta correct.

    Thank you so much for making me focus on preFetch.

    I will just figure out how to “allow” Axios calls to local Lando/Docker API or maybe just disable preFetch locally since the meta isn´t that important under development.



  • By the way, console.log(error) when running preFetch in my dev env gives me this error:

    Error: connect ECONNREFUSED 172.20.0.4:80
        at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1054:14) {
      errno: 'ECONNREFUSED',
      code: 'ECONNREFUSED',
      syscall: 'connect',
      address: '172.20.0.4',
      port: 80,
      config: {
        url: 'api/site/getPostObj',
        method: 'get',
        headers: {
          Accept: 'application/json, text/plain, */*',
          'X-Client-Api-Key': 'XXXXXXXXXXXXXXXXXX,
          'Content-Type': 'application/json',
          'User-Agent': 'axios/0.21.0'
        },
        params: { post_slug: 'produktunderkategori-nyhet' },
        baseURL: 'http://api-lsvab.lndo.site',
        transformRequest: [ [Function: transformRequest] ],
        transformResponse: [ [Function: transformResponse] ],
        timeout: 180000,
        adapter: [Function: httpAdapter],
        xsrfCookieName: 'XSRF-TOKEN',
        xsrfHeaderName: 'X-XSRF-TOKEN',
        maxContentLength: -1,
        maxBodyLength: -1,
        validateStatus: [Function: validateStatus],
        data: undefined
      },
      request: Writable {
        _writableState: WritableState {
          objectMode: false,
          highWaterMark: 16384,
          finalCalled: false,
          needDrain: false,
          ending: false,
          ended: false,
          finished: false,
          destroyed: false,
          decodeStrings: true,
          defaultEncoding: 'utf8',
          length: 0,
          writing: false,
          corked: 0,
          sync: true,
          bufferProcessing: false,
          onwrite: [Function: bound onwrite],
          writecb: null,
          writelen: 0,
          bufferedRequest: null,
          lastBufferedRequest: null,
          pendingcb: 0,
          prefinished: false,
          errorEmitted: false,
          emitClose: true,
          autoDestroy: false,
          bufferedRequestCount: 0,
          corkedRequestsFree: [Object]
        },
        writable: true,
        _events: [Object: null prototype] {
          response: [Array],
          error: [Function: handleRequestError],
          timeout: [Function]
        },
        _eventsCount: 3,
        _maxListeners: undefined,
        _options: {
          maxRedirects: 21,
          maxBodyLength: 10485760,
          protocol: 'http:',
          path: '/api/site/getPostObj?post_slug=produktunderkategori-nyhet',
          method: 'GET',
          headers: [Object],
          agent: undefined,
          agents: [Object],
          auth: undefined,
          hostname: 'api-lsvab.lndo.site',
          port: null,
          nativeProtocols: [Object],
          pathname: '/api/site/getPostObj',
          search: '?post_slug=produktunderkategori-nyhet'
        },
        _ended: true,
        _ending: true,
        _redirectCount: 0,
        _redirects: [],
        _requestBodyLength: 0,
        _requestBodyBuffers: [],
        _onNativeResponse: [Function],
        _currentRequest: ClientRequest {
          _events: [Object: null prototype],
          _eventsCount: 7,
          _maxListeners: undefined,
          outputData: [],
          outputSize: 0,
          writable: true,
          _last: true,
          chunkedEncoding: false,
          shouldKeepAlive: false,
          useChunkedEncodingByDefault: false,
          sendDate: false,
          _removedConnection: false,
          _removedContLen: false,
          _removedTE: false,
          _contentLength: 0,
          _hasBody: true,
          _trailer: '',
          finished: true,
          _headerSent: true,
          socket: [Socket],
          connection: [Socket],
          _header: 'GET /api/site/getPostObj?post_slug=produktunderkategori-nyhet HTTP/1.1\r' +
            '\nAccept: application/json, text/plain, */*\r' +
            '\nX-Client-Api-Key: XXXXXXXXXXXXXXXXX\r' +
            '\nContent-Type: application/json\r' +
            '\nUser-Agent: axios/0.21.0\r' +
            '\nHost: api-lsvab.lndo.site\r' +
            '\nConnection: close\r' +
            '\n\r' +
            '\n',
          _onPendingData: [Function: noopPendingOutput],
          agent: [Agent],
          socketPath: undefined,
          method: 'GET',
          path: '/api/site/getPostObj?post_slug=produktunderkategori-nyhet',
          _ended: false,
          res: null,
          aborted: false,
          timeoutCb: null,
          upgradeOrConnect: false,
          parser: null,
          maxHeadersCount: null,
          _redirectable: [Circular],
          [Symbol(isCorked)]: false,
          [Symbol(outHeadersKey)]: [Object: null prototype]
        },
        _currentUrl: 'http://api-lsvab.lndo.site/api/site/getPostObj?post_slug=produktunderkategori-nyhet',
        _timeout: Timeout {
          _idleTimeout: -1,
          _idlePrev: null,
          _idleNext: null,
          _idleStart: 29830654,
          _onTimeout: null,
          _timerArgs: undefined,
          _repeat: null,
          _destroyed: false,
          [Symbol(refed)]: null,
          [Symbol(asyncId)]: 796876,
          [Symbol(triggerId)]: 796873
        }
      },
      response: undefined,
      isAxiosError: true,
      toJSON: [Function: toJSON]
    }
    

    any idea how to solve this?



  • @PeterQF I’m not really well versed in docker. Is your SSR server and API server on different containers I suppose? You may need to set something with hosts to allow the SSR process to reach the API.

    For me, without docker, I set baseURL to 127.0.0.1:8000 on the server, and https://api.example.com client. So if you can access the api with an internal network IP that could work, as long as docker is set up right. In this case you would have four checks:

    If production and is server
    If production and is client
    If staging and is server
    If staging and is client

    And choose a host name / ip that can be reached on each of those cases.

    As for the Host header, if you weren’t restricting api calls by that header, you don’t need it. But if you are, then just don’t set that on client. What I would do is some like:

    const axiosConfig = {
    headers:{
    // your api key, and common stuff
    }
    }

    if(process.env.SERVER) {
    axiosConfig.headers.Host = “frontendurl.com
    if(process.env.NODE_ENV === “development”) {
    axiosConfig.baseURL = “something”
    } else {
    axiosConfig.baseURL = “something else”
    }
    } else {
    // repeat dev or prod check from above
    }

    On mobile, excuse any typos



  • @PeterQF also, yes, prefetch runs on either client or server. Initial page load causes prefetch to happen on server, while navigating to that page after the initial load causes it to happen on the client



  • So guys,
    This is how i solved this.

    Turned out that Lando/Docker needed another base url when connecting to server with preFetch so the simple solution was to use this code for baseURL:

    baseURL: process.env.PROD
        ? 'https://api.lsvab.se'
        : process.env.SERVER && process.env.DEV ? 'http://api-server.apilsvab.internal:8000' : 'http://api-lsvab.lndo.site',
    

    Hope it can help anyone using Lando/Docker.



  • The problem with docker is that binding to the 127.0.0.1:xxx will not work - you must bind to 0.0.0.0:xxx. It is well known trap



  • Well,
    Pure Docker might accept 0.0.0.0:xxx but not using Lando.
    With Lando one has to connect to xxx.internal:xxxx

    Just like with DB connection: DB_HOST=default.postgres96.internal



  • Connection target and binding one are different things. At docker you must bind server to 0.0.0.0 and connect client using node host address which is usually internal to docker network



  • @Sfinx i’m sorry if i mix things up.
    I’m really not that skilled with Docker, that is why i use Lando as it takes care of all this.
    Just had to figure out how Lando wants me to do Axios calls in different situations.


Log in to reply