I am a huge Ember.js fan, but recently wanted to experiment with some other frameworks, and decided to try Nuxt.js. I was very pleasantly surprised, that Nuxt had many nice "magic" things, just like Ember had, and felt very familiar to Ember development. There were a lot of additional nice features as well, like built in PWA support, static site generation, tree shaking, and code splitting. With all of these awesome features, I decided to convert shipshape.io from Ember to Nuxt, and wanted to document the mappings between things in Ember and Nuxt and the benefits and drawbacks of each.

Table of Contents

  1. Application Wrapper
  2. Components
  3. Routes
  4. Static Site Generation
  5. Meta
  6. PWA
  7. Sitemaps
  8. Code Splitting, Tree Shaking, and PurgeCSS
  9. Summary

Application Wrapper

templates/application.hbs -> layouts/default.vue

In Ember.js, you will typically setup your main application wrapper, with things like your navbar, footer, etc in the templates/application.hbs file. In Nuxt.js you have the concept of layouts, and layouts/default.vue is where you define your application wrapper markup.

{{outlet}} -> <Nuxt/>

In Nuxt, you'll define where your application content is inserted with <Nuxt/>, rather than the {{outlet}} you typically have in Ember.

Example

Ember.js
{{! templates/application.hbs}}

{{head-layout}}

<div 
  itemscope 
  itemtype="https://schema.org/Organization" 
  itemid="shipshapeorg"
>
  <NavMenu/>

  <main>
    {{outlet}}
  </main>

  <WaveFooter/>
</div>
Nuxt.js
<!-- layouts/default.vue -->

<template>
  <div
    itemscope
    itemtype="https://schema.org/Organization"
    itemid="shipshapeorg"
  >
    <meta itemprop="legalName" content="Ship Shape Consulting LLC">

    <NavMenu/>

    <main>
      <Nuxt/>
    </main>

    <WaveFooter/>
  </div>
</template>

<script>
  import NavMenu from '~/components/NavMenu.vue';
  import WaveFooter from '~/components/WaveFooter.vue';

  export default {
    components: {
      NavMenu,
      WaveFooter
    }
  };
</script>

Components

With the new angle bracket syntax for Ember's Glimmer components, copying and pasting components into Nuxt/Vue becomes much easier. Especially with the addition of Tailwind CSS, I did not have to worry much about specific styles for each component.

components/blog-post/component.js + components/blog-post/template.hbs -> components/BlogPost.vue

In Ember you have separate JS and template files for components, typically housed either in the components or templates/components directory. In Nuxt you have just one file, containing the template, script, and styles for the component. In most cases, the bulk of the code can be directly copied over.

{{yield}} -> <slot/>

{{yield}} is used to pass through the contents of a block component in Ember, and in Nuxt you will replace it with <slot/> instead.

Example

Ember.js
// components/blog-post/component.js

import Component from '@ember/component';
import { className, tagName } from '@ember-decorators/component';
import { alias } from '@ember-decorators/object/computed';
import { htmlSafe } from '@ember/template';
import { set } from '@ember/object';

@tagName('article')
export default class BlogPost extends Component {
  @alias('post.author')
  author;

  @alias('post.attributes.date')
  date;

  @alias('post.attributes.nextSlug')
  nextSlug;

  @alias('post.attributes.nextTitle')
  nextTitle;

  @alias('post.attributes.previousSlug')
  previousSlug;

  @alias('post.attributes.previousTitle')
  previousTitle;

  @alias('post.attributes.slug')
  @className
  slug;

  @alias('post.attributes.title')
  title;

  didReceiveAttrs() {
    super.didReceiveAttrs(...arguments);
    set(this, 'content', htmlSafe(this.post.attributes.html));
  }

  didRender() {
    super.didRender(...arguments);

    let nodeList = this.element.querySelectorAll('pre:not(.no-line-numbers) > code');

    if (nodeList) {
      // console.log(nodeList);
      nodeList.forEach((code) => {
        code.parentNode.classList.add('line-numbers');
      });
    }

    Prism.highlightAll();
  }
}
{{! components/blog-post/template.hbs }}

<div itemscope itemtype="http://schema.org/BlogPosting">
  <link itemprop="publisher" href="shipshapeorg">
  <link itemprop="image" href="shipshapelogo">

  <div class="section flex flex-wrap justify-center">
    <div class="max-w-lg w-full">
      <h1 class="blog-post-title" itemprop="headline">
        {{this.title}}
      </h1>

      <AuthorRow
        @author={{this.author}}
        @date={{this.date}}
      >
      </AuthorRow>

      <div class="post-content">
        {{this.content}}
      </div>

      <BottomLinksWithPath
        @nextLink="blog.post"
        @nextLinkPath={{this.nextSlug}}
        @nextLinkText={{this.nextTitle}}
        @previousLink="blog.post"
        @previousLinkPath={{this.previousSlug}}
        @previousLinkText={{this.previousTitle}}
      >
      </BottomLinksWithPath>
    </div>
  </div>
</div>
Nuxt.js
<!-- components/BlogPost.vue -->
<template>
  <article itemscope itemtype="http://schema.org/BlogPosting">
    <link itemprop="mainEntityOfPage" :href="$nuxt.$route.path">
    <link itemprop="publisher" href="shipshapeorg">
    <link itemprop="image" href="shipshapelogo">

    <div class="section flex flex-wrap justify-center">
      <div class="max-w-lg w-full">
        <h1 class="blog-post-title" itemprop="headline">
          {{ post.title }}
        </h1>

        <AuthorRow
          v-bind="post.author.attributes"
          :date="post.date"
        />

        <div
          class="post-content"
          v-html="post.html"
        />

        <BottomLinks
          :next-link="`/blog/${post.nextSlug}/`"
          :next-link-text="post.nextTitle"
          :previous-link="`/blog/${post.previousSlug}/`"
          :previous-link-text="post.previousTitle"
        />
      </div>
    </div>
  </article>
</template>

<script>
  import AuthorRow from '~/components/AuthorRow.vue';
  import BottomLinks from '~/components/BottomLinks.vue';

  export default {
    components: {
      AuthorRow,
      BottomLinks
    },

    props: {
      post: {
        type: Object,
        default: () => {}
      }
    }
  };
</script>

As you can see, the markup is very similar for both the Ember and Nuxt components. There are small differences, but in most cases you can change things like defining arguments to the component with @foo in Ember, to using :foo in Nuxt, and it will work.

Routes

routes/blog/index.js + templates/blog/index.hbs -> pages/blog/index.vue

In Ember, your routes have separate JS and hbs files, but in Nuxt you put your JavaScript, template, and styles all in one file. I find this to be a big downside of Nuxt, and would really prefer to keep separate files for everything.

model -> asyncData

In Ember, you will typically do all of your data fetching in the model hook. Nuxt has a similar concept in its asyncData method, which will load all the data server side, allowing you to do async things, before setting the component data, much like Ember waits for the model hook to return, before rendering the page.

Example

Ember.js
// routes/blog/index.js

import Route from '@ember/routing/route';
import asyncForEach from 'ember-async-await-for-each';
import fetch from 'fetch';

export default class Blog extends Route {
  async model() {
    let authors = await fetch('/authors/authors.json');
    authors = await authors.json();
    authors = authors.data;

    let posts = await fetch('/posts/posts.json');
    posts = await posts.json();
    posts = posts.data;

    await asyncForEach(posts, async (post) => {
      post.author = await authors.find((author) => {
        return author.id === post.attributes.authorId;
      });
    });

    return posts.sort((post1, post2) => {
      if(post1.attributes.date > post2.attributes.date){
        return -1;
      }

      if(post1.attributes.date < post2.attributes.date){
        return 1;
      }

      return 0;
    });
  }
}
<div class="blog-posts section flex flex-wrap justify-center">
  <div class="section-content">
    <div class="flex items-center">
      <h1>Blog</h1>

      <a
        class="p-12"
        href="https://feedly.com/i/subscription/feed%2Fhttps%3A%2F%2Fshipshape.io%2Ffeed.xml"
        target="_blank"
        rel="noopener"
      >
        {{svg-jar "rss"}}
      </a>
    </div>

    <p>
      Ramblings about Ember.js, JavaScript, life, liberty, and the pursuit of happiness.
    </p>

    <BlogPostMenu
      @navigatePages={{action "navigatePages"}}
      @page={{this.page}}
      @posts={{this.model}}
      @totalPosts={{this.model.length}}
    >
    </BlogPostMenu>
  </div>
</div>
Nuxt.js
<template>
  <div class="blog-posts section flex flex-wrap justify-center">
    <div class="section-content">
      <div class="flex items-center">
        <h1>
          Blog
        </h1>

        <a
          class="p-12"
          href="https://feedly.com/i/subscription/feed%2Fhttps%3A%2F%2Fshipshape.io%2Ffeed.xml"
          target="_blank"
          rel="noopener"
        >
          <RSS/>
        </a>
      </div>

      <p>
        Ramblings about Ember.js, JavaScript, life, liberty, and the pursuit of happiness.
      </p>

      <BlogPostMenu :posts="posts"/>
    </div>
  </div>
</template>

<script>
  import BlogPostMenu from '~/components/BlogPostMenu.vue';
  import RSS from '~/assets/svgs/rss.svg?inline';
  import { getBlogData } from '~/utils/blog';
  import { generateMeta } from '~/utils/meta';

  export default {
    scrollToTop: true,

    components: {
      BlogPostMenu,
      RSS
    },

    asyncData() {
      return getBlogData();
    },

    head() {
      const title = 'Blog';
      const description = 'Ramblings about Ember.js, JavaScript, life, liberty, and the pursuit of happiness.';
      const url = 'https://shipshape.io/blog/';

      return generateMeta(title, description, url);
    }
  };
</script>

Static Site Generation

Ember has a nice addon, Prember, that allows you to turn your Ember app into a static site. Static site generation is built into Nuxt out of the box, so you can run yarn generate to get a static version of your site.

Meta

In Ember, there are a few ways to add meta tags for your pages, but ember-meta is arguably the most popular addon, and the addon used for the Ember.js website meta.

In Nuxt, meta has first class support, and you utilize the built in head property to set your meta for each page.

PWA

In Ember, you will need to install several service worker addons to get offline support and caching, but in Nuxt this is all built in to the framework, which is super nice because you do not have to worry about any of the service worker internals, and you know the framework has bought into the idea and will continue to support it as a first class feature. You do need to ensure @nuxtjs/pwa is installed, but other than that, it is zero config.

Sitemaps

Sitemaps in Ember and Nuxt are very similar, and both require the addition of a plugin to generate them. In Ember we use prember-sitemap-generator and in Nuxt we use @nuxtjs/sitemap. Unless you have no dynamic routes, both require that you pass the urls for all of your pages in, and output the resulting sitemap.

Code Splitting, Tree Shaking, and PurgeCSS

Features like code splitting and tree shaking have been experimented with in Ember and efforts to support them are in progress, however they are not currently usable or stable. Additionally, due to the dynamic nature of classes in Ember, and the lack of explicit template imports, it is currently not possible to use PurgeCSS, without a lot of manual work.

Code Splitting, Tree Shaking and PurgeCSS all work out of the box with Nuxt, and the only additional thing to install is the nuxt-purgecss module.

Summary

There is no right or wrong framework to use, only what you decide works best for your project. However, it is nice to see how much things are beginning to overlap in modern frameworks, and how copying and pasting code between them is becoming more and more viable. This really solidifies my belief that both Ember and Nuxt are on the right track, and I am excited to see where each of them goes in the coming years!