Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
nathanchase opened this issue Jun 18, 2018 · 9 comments
Open

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

nathanchase opened this issue Jun 18, 2018 · 9 comments
Labels

Comments

@nathanchase
Copy link

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
Copy link
Contributor

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

@nathanchase
Copy link
Author

nathanchase commented Jul 3, 2018

@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
Copy link
Member

fkleuver commented Jul 3, 2018

Just so I understand this correctly, you want to:

  • Add meta tags to the router configuration from the client side
  • The precise meta tags that you're adding depends on data you get from the server side
  • The server should extract this information and pre-render the HTML head accordingly for the next page you navigate to (from the client side)

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
Copy link
Author

nathanchase commented Jul 4, 2018

@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
Copy link
Member

fkleuver commented Jul 4, 2018

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
Copy link
Author

@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
Copy link
Author

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
Copy link
Member

fkleuver commented Jul 4, 2018

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
Copy link
Author

nathanchase commented Jul 4, 2018

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants