Friendly (slug) URLs instead of ID

I’m just getting started with Ember and I’m trying to set up friendly URLs for an example recipes app (with Firebase/emberfire).

I’m hoping to be able to get URLs that look something like /recipes/beans-on-toast. So far, everything’s working if I arrive at the URL from a {{#link-to}}, but if I visit the URL directly, it won’t work.

Various sources have suggested the serialize() hook as being something that needs to be configured, but hasn’t appeared to help.

Have I overlooked something basic?

Here’s my setup:

// models/recipe.js
import DS from 'ember-data';

export default DS.Model.extend({
  title: DS.attr('string'),
  slug: DS.attr('string'),
  description: DS.attr('string'),
});

// routes/recipe.js
import Ember from 'ember';

export default Ember.Route.extend({
  model(params) {
    return this.store.find('recipe', params.slug);
  },

  // Not sure if this is necessary
  serialize(model) {
    return {
      slug: model.get('slug')
    };
  }
});


// router.js
import Ember from 'ember';
import config from './config/environment';

const Router = Ember.Router.extend({
  location: config.locationType
});

Router.map(function() {
  this.route('recipes');
  this.route('recipe', {path: '/recipes/:slug'});
});

export default Router;

Serialize is the piece that computes the url part from the model. It is indeed required. It is the reason it works when you navigate in your site.

The model hook does the reverse: convert the url part into an actual model. Here, you feed the slug into a find request. Set up like this, it will perform a GET on /recipe/some-slug/. So for it to work, you would need your API to also use the slug in its url construction.

Or, you could change the model constructor to do another query, for instance find('recipe', {slug: params.slug}) should perform a GET on /recipe/?slug=some-slug

2 Likes

There’s actually a specific Ember Data hook for this: Store - 4.6 - Ember API Documentation

@spectras is right that you’ll need your API to handle the slug, but you should also signal to Ember data that you’re only expecting a single record in the response by using queryRecord, otherwise your model hook is returning an array.

(FYI: Not sure when queryRecord landed, but it was recent-ish…if you’re on an older version of Ember data, it may not be available)

Nice – thank you both.

With Firebase’s querying options, I’ve got:

model(params) {
  return this.store.query('recipe', {
    orderBy: 'slug',
    equalTo: params.slug
  });
},

This works, but returns an array (and I get “Error: Assertion Failed: You tried to make a query but your adapter does not implement queryRecord” if I try queryRecord).

Should I just use an {{#each}} to output the single item, or is there anything else I could do to only provide the single object to the template?

edit: I’ve gone with this:

model(params) {
  return this.store.query('recipe', {
    orderBy: 'slug',
    equalTo: params.slug
  }).then(function(data) {
    return data.get('firstObject');
  });
}

It works, but I’ve no idea how gross/much of an anti-pattern it might be. All pointers gratefully accepted.

1 Like

I guess you could extend the adapter/serializer and include your code for extracting the item in it, so you don’t have to repeat it everytime (also it is the object that’s normally responsible for converting data, so it belongs there).

1 Like

Since you’re on a version without queryRecord — your solution is exactly how I’ve handled it in the past. :slight_smile:

1 Like

Any danger of spoonfeeding me how to extend the adapter/serializer, or pointing me in the direction of an appropriate blog post?

Unfortunately, I’m not using ember-data myself (I use an older project named ember-model - planning to switch at some point but it is quite an undertaking). Its concepts are close enough that I can answer common questions, but more advanced uses don’t work the same.

A quick search leads to this: Fit Any Backend Into Ember with Custom Adapters & Serializers - Ember Igniter, maybe it will be helpful.

1 Like