aurelia / router

A powerful client-side router.
MIT License
121 stars 115 forks source link

How to use router navModel to inject metatags server-side? #604

Open nathanchase opened 6 years ago

nathanchase commented 6 years ago

Is there a way to create a method like routeConfig.navModel.setTitle(), but for adding meta tags to the head for SSR?

Essentially, I need a way to add html meta tags server-side (description, OpenGraph, Twitter, etc.) for each route - but it needs to be dynamic and dependent entirely on API-retrieved data.

So something like (not real code, just approximate):

var metatags = 
// hit api, get JSON data, create object like ->
page: {
      description: 'my description',
      keywords: 'fun, easy, cool'
    },
opengraph: {
      'og:type': 'article',
      'og:image': 'https://mysite.com/images/image.jpg,
      'og:image:height': 422,
      'og:image:width': 806,
      'og:title': 'My Website',
      'og:site_name': 'Website Name',
      'og:description': 'my description',
      'og:url': 'https://mysite.com'
}

activate(params, routeConfig){
  routeConfig.navModel.setMeta(this.metatags);
}
Alexander-Taran commented 6 years ago

start here https://discourse.aurelia.io/t/server-side-rendering-improvements/883

nathanchase commented 6 years ago

@Alexander-Taran Yes, I'm already on that thread, but rquast was going a different route (tying data statically to each route via the router config) whereas I need the data populated dynamically depending on API data. I haven't been able to find any way to do this anywhere online with Aurelia. It seems like it's no big deal in the Vue (vue-meta)/React (react-helmet) camps, so I'm trying to figure out how exactly to do it in Aurelia.

fkleuver commented 6 years ago

Just so I understand this correctly, you want to:

To rephrase: metadata goes from the server to the client, is assigned/distributed on the client, which the server then needs in order to pre-render a page that isn't actually loaded from the server since you're navigating to it from the client side.

I must be misinterpreting you somewhere but that's how I'm reading it. Please correct me if I'm wrong :)

nathanchase commented 6 years ago

@fkleuver Using SSR, running Aurelia server-side, the requested page/route is rendered with metatags that are dynamically populated by whatever route is requested.

Example: myapp.com/user/JohnDoe

Aurelia should render the user view, then takes the dynamic value "JohnDoe", sends to the API, pulls appropriate information about that page, then adds it to the <head>.

Then it gets rendered server-side in HTML before the client-side Aurelia begins.

This is a mandatory need for my app, as I have thousands of individual pages with SEO requirements AND must be deep-linkable via OpenGraph (Facebook) and Twitter.

JSON-LD client-side isn't good enough, and even Google's JS rendering isn't good enough. Neither of those options are supported by Facebook or Twitter (or other future social share use cases).

fkleuver commented 6 years ago

Ah that makes sense. I guess we wouldn't need to do anything specific in aurelia-router as this information (like any other arbitrary info) can just be added to a route config's settings property.

Perhaps accompanied by some SSR configuration to tell SSR on which property it needs to look. Then it can't accidentally do unexpected things when people use a particular settings property for other purposes that just happens to match the (metadata?) name we look for.

In any case not much (or nothing at all) can/should be done in aurelia-router to accomodate this. The server must figure out what to do with the info stored in route configs and carry out the appropriate mutations. That's just how I see it though. Maybe someone else has a better idea.

nathanchase commented 6 years ago

@fkleuver Well, look at vue-meta's implementation:

<template>
  ...
</template>

<script>
  export default {
    metaInfo: {
      title: 'My Example App', // set a title
      titleTemplate: '%s - Yay!', // title is now "My Example App - Yay!"
      htmlAttrs: {
        lang: 'en',
        amp: undefined // "amp" has no value
      }
    }
  }
</script>

I would think you could pull the dynamic route slug off the route, use isomorphic-fetch to send it to the API to get back the metadata needed, and then that "metadata" object is injected into the head before Aurelia renders the page.

The thing is, I can't use route.config, because that would be static only.

Rquast's "transformer-step" code is close, but it's pulling from the route.config for the data, whereas I just need it to fetch from an API instead.

The aurelia-router already has a title property, so I thought it made sense that it should potentially have access to populate the entire <head> as needed - particularly now that SSR is "shipped".

nathanchase commented 6 years ago

The "ssr-transformer":

import {DOM} from 'aurelia-pal';

export const TRANSFORMER_TYPES = {
  page: {
    type: 'meta',
    key: 'name',
    value: 'content',
    elements: [
      'description',
      'keywords'
    ]
  },
  opengraph: {
    type: 'meta',
    key: 'property',
    value: 'content',
    elements: [
      'og:type',
      'og:image',
      'og:image:height',
      'og:image:width',
      'og:title',
      'og:site_name',
      'og:description',
      'og:url'
    ]
  }
};

/**
 * Mutates header elements with an attribute of au-ssr-id with data from a set of variables
 */
export default {

  variables: {},

  headers: {},

  getElements: function(name, config) {
    let transformer = TRANSFORMER_TYPES[name];
    let elements = [];

    for (let key of transformer.elements) {
      let el = DOM.createElement(transformer.type);
      el.setAttribute(transformer.key, key);
      el.setAttribute(transformer.value, config[key]);
      el.setAttribute('au-ssr-id', name + '.' + key);
      elements.push(el);
    }

    return elements;
  },

  appendElements: function() {
    let head = DOM.querySelectorAll('head');

    for (let name in this.variables) {
      if (this.variables.hasOwnProperty(name)) {
        let elements = this.getElements(name, this.variables[name]);
        for (let element of elements) {
          head[0].appendChild(element);
        }
      }
    }
  },

  removeElements: function() {
    let head = DOM.querySelectorAll('head');
    let nodes = head[0].querySelectorAll('[au-ssr-id]');
    for (let node of nodes) {
      DOM.removeNode(node, head[0]);
    }
  },

  mutate: function(config) {
    if (config.variables) {
      this.variables = config.variables;
    } else {
      // take the home route variables
      this.variables = config.navModel.router.routes[0].variables;
    }
    if (typeof window !== 'undefined') {
      // remove elements each time a browser loads or route changes
      this.removeElements();
    }
    this.appendElements();
  }

};

https://gist.github.com/rquast/a9cbc0551a48d10e83b2ad899b293c77

fkleuver commented 6 years ago

The aurelia-router already has a title property, so I thought it made sense that it should potentially have access to populate the entire as needed - particularly now that SSR is "shipped".

The thing is that the page title can changed via document.title (it doesn't actually change the title metadata tag in the HTML file).

That's a key difference that makes title different from those other properties, which can only be changed by modifying the html. Modifying the HTML is not something that the router should do, hence this is not really the right place to be doing that. I suppose we could add a ssrProperties property to RouteConfig so that it's clear that it won't do anything without SSR. Does that make sense?

nathanchase commented 6 years ago

So, something like:

import fetch = require('isomorphic-fetch');

activate(params, routeConfig) {  
  fetch('//myapp.com/api/contacts/' + params.id)
    .then(function(response) {
        return response.json();
        }).then(function(metadata) {
                routeConfig.navModel.ssrProperties(data: metadata};
  });
}

?

How exactly would we be able to add tags to the HTML via the ssrProperties property? How would the ssrProperties object parse the incoming data?

I really appreciate the discussion and assistance, as I know there must be many developers in the same boat with SEO requirements for their apps.