How to create custom pages in nuxt

2 February 2021
10 min read

We have previously discussed how to use and create custom layouts in nuxt and we learned how useful how helpful they are in helping you design and change the general look and feel of your website. The pages directory is the directory to create all your application views and routes. Nuxt iterates through all the files and directories in this folder to automatically create the router configuration for you.

Every Page component is a Vue component but Nuxt adds special attributes and functions to make the development of your application as easy as possible. If we start with a very simple page example we'll develop it further as we move through this tutorial.

<!-- pages/hello-world.vue -->
<template>
  <p>Hello World!</p>
</template>

<script>
  export default {
    // page properties go here
  }
</script>

If we start our project using npm run dev and then open our brower and visit http://locahost:3000/hello-world we'll see our new page in all it glory!

Hello World Nuxt

That's great, we've created our first page using nuxt and we can view it. The problem is it doesn't look like much and it does absolutely nothing. However, Nuxt on the other hand has done quite a bit for us in the background.

Let's carry out some modification to the page, to see exactly what Nuxt has done with the page in the background.

As you will be no doubt aware that Nuxt is an application software development framework built on top of Vue, and pages are in effect nothing more than Vue components and when we created our simple Hello-World page, we created it as a hello-world.vue . However, what Nuxt has done with this vue component is actually empower with this some additional functions that will help us to easily modify our pages to be dynamic and enable dynamic content.

Understanding pages

A page is a Vue component in nature. What sets it apart from the standard Vue component is the attributes and functions that are added in Nuxt only. We use these special attributes and functions to set or fetch data before rendering the page.

We can slightly modify our page now to make use of the one of the functions that only Nuxt supplies us with the asyncData function.

The asyncData method is the most important function in a page component. Nuxt always initiate this function before the page component. Therefore, every time you request a page, this function is called first before the page is rendered.

<template>
  <p>{{ message }}</p>
</template>

<script>
export default {
  asyncData() {
    return { message: 'Hello World!' }
  },
}
</script>

When we now view this page in the browser we'll not notice nay difference, but in reality a lot has changed. Our page has just become a little more dynamic.

In fact, one of the arguments the asyncData command accepts the context and this object will contain a lot of useful information about our application. The below is an example of the type of data and properties that the context object contains even for our very simple page!

{                                                                                                                                                                                              20:27:42
  isStatic: true,
  isDev: true,
  isHMR: false,
  app: {
    head: {
      title: 'geekiam-nuxt-tutorial',
      meta: [Array],
      link: [Array],
      style: [],
      script: [],
      htmlAttrs: [Object]
    },
    router: VueRouter {
      app: [Vue],
      apps: [Array],
      options: [Object],
      beforeHooks: [],
      resolveHooks: [],
      afterHooks: [],
      matcher: [Object],
      fallback: false,
      mode: 'abstract',
      history: [AbstractHistory],
      resolve: [Function]
    },
    nuxt: {
      defaultTransition: [Object],
      transitions: [Array],
      setTransitions: [Function: setTransitions],
      err: null,
      dateErr: null,
      error: [Function: error]
    },
    render: [Function: render],
    data: [Function: data],
    beforeCreate: [Function: beforeCreate],
    created: [Function: created],
    mounted: [AsyncFunction: mounted],
    watch: {
      'nuxt.err': 'errorChanged'
    },
    computed: {
      isOffline: [Function: isOffline],
      isFetching: [Function: isFetching],
      isPreview: [Function: isPreview]
    },
    methods: {
      refreshOnlineStatus: [Function: refreshOnlineStatus],
      refresh: [AsyncFunction: refresh],
      errorChanged: [Function: errorChanged],
      setLayout: [Function: setLayout],
      loadLayout: [Function: loadLayout]
    },
    components: {
      NuxtLoading: [Object]
    },
    context: [Circular],
    '$config': {},
    '$content': [Function: $content] {
      database: [Database]
    },
    '$icon': [Function: getIcon],
    '$axios': [Function: wrap] {
      request: [Function: wrap],
      getUri: [Function: wrap],
      delete: [Function: wrap],
      get: [Function: wrap],
      head: [Function: wrap],
      options: [Function: wrap],
      post: [Function: wrap],
      put: [Function: wrap],
      patch: [Function: wrap],
      defaults: [Object],
      interceptors: [Object],
      CancelToken: [Function],
      isCancel: [Function: isCancel],
      setBaseURL: [Function: bound setBaseURL],
      setHeader: [Function: bound setHeader],
      setToken: [Function: bound setToken],
      onRequest: [Function: bound onRequest],
      onResponse: [Function: bound onResponse],
      onRequestError: [Function: bound onRequestError],
      onResponseError: [Function: bound onResponseError],
      onError: [Function: bound onError],
      create: [Function: bound create],
      '$request': [Function: bound ],
      '$delete': [Function: bound ],
      '$get': [Function: bound ],
      '$head': [Function: bound ],
      '$options': [Function: bound ],
      '$post': [Function: bound ],
      '$put': [Function: bound ],
      '$patch': [Function: bound ]
    }
  },
  payload: undefined,
  error: [Function: bound error],
  base: '/',
  env: {},
  ssrContext: {
    req: IncomingMessage {
      _readableState: [ReadableState],
      readable: true,
      _events: [Object: null prototype],
      _eventsCount: 1,
      _maxListeners: undefined,
      socket: [Socket],
      connection: [Socket],
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      headers: [Object],
      rawHeaders: [Array],
      trailers: {},
      rawTrailers: [],
      aborted: false,
      upgrade: false,
      url: '/hello-world',
      method: 'GET',
      statusCode: null,
      statusMessage: null,
      client: [Socket],
      _consuming: false,
      _dumped: false,
      originalUrl: '/hello-world',
      _parsedUrl: [Url],
      _parsedOriginalUrl: [Url]
    },
    res: ServerResponse {
      _events: [Object: null prototype],
      _eventsCount: 1,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      _last: false,
      chunkedEncoding: false,
      shouldKeepAlive: true,
      useChunkedEncodingByDefault: true,
      sendDate: true,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      _contentLength: null,
      _hasBody: true,
      _trailer: '',
      finished: false,
      _headerSent: false,
      socket: [Socket],
      connection: [Socket],
      _header: null,
      _onPendingData: [Function: bound updateOutgoingData],
      _sent100: false,
      _expect_continue: false,
      locals: {},
      statusCode: 200,
      [Symbol(kNeedDrain)]: false,
      [Symbol(isCorked)]: false,
      [Symbol(kOutHeaders)]: null
    },
    url: '/hello-world',
    target: 'static',
    spa: undefined,
    modern: false,
    runtimeConfig: {
      private: {},
      public: {}
    },
    '$content': [Function: $content] {
      database: [Database]
    },
    _registeredComponents: Set {},
    styles: [Getter],
    _renderStyles: [Function: renderStyles],
    _styles: {
      '0211d3d2:0': [Object],
      '517a8dd7:0': [Object]
    },
    redirected: false,
    next: [Function],
    beforeRenderFns: [],
    nuxt: {
      layout: 'default',
      data: [],
      fetch: [],
      error: null,
      serverRendered: true,
      routePath: '/hello-world',
      config: {}
    },
    meta: {
      getOptions: [Function: getOptions$1],
      setOptions: [Function: setOptions],
      refresh: [Function: refresh$1],
      inject: [Function: inject$1],
      pause: [Function: pause$1],
      resume: [Function: resume$1],
      addApp: [Function: addApp$1]
    },
    asyncData: {}
  },
  redirect: [Function],
  beforeNuxtRender: [Function],
  route: {
    name: 'hello-world',
    meta: [
      {}
    ],
    path: '/hello-world',
    hash: '',
    query: {},
    params: {},
    fullPath: '/hello-world',
    matched: [
      [Object]
    ]
  },
  next: [Function],
  _redirected: false,
  _errored: false,
  params: {},
  query: {},
  '$config': {},
  '$content': [Function: $content] {
    database: Database {
      _logger: [Object],
      _hooks: [Object],
      _deprecatedHooks: {},
      hook: [Function: bound hook],
      callHook: [Function: bound callHook] AsyncFunction,
      dir: '/home/gary/code/geekiam-nuxt-tutorial/content',
      cwd: '/home/gary/code/geekiam-nuxt-tutorial',
      markdown: [Markdown],
      yaml: [YAML],
      csv: [CSV],
      xml: [XML],
      db: [Loki],
      items: [collection_Collection],
      extendParser: {},
      extendParserExtensions: [],
      queue: [PQueue],
      watcher: [FSWatcher],
      options: [Object],
      dirs: [Array]
    }
  },
  '$icon': [Function: getIcon],
  '$axios': [Function: wrap] {
    request: [Function: wrap],
    getUri: [Function: wrap],
    delete: [Function: wrap],
    get: [Function: wrap],
    head: [Function: wrap],
    options: [Function: wrap],
    post: [Function: wrap],
    put: [Function: wrap],
    patch: [Function: wrap],
    defaults: {
      headers: [Object],
      baseURL: 'http://localhost:3000/',
      transformRequest: [Array],
      transformResponse: [Array],
      timeout: 0,
      adapter: [Function: httpAdapter],
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      maxBodyLength: -1,
      validateStatus: [Function: validateStatus]
    },
    interceptors: {
      request: [InterceptorManager],
      response: [InterceptorManager]
    },
    CancelToken: [Function: CancelToken] {
      source: [Function: source]
    },
    isCancel: [Function: isCancel],
    setBaseURL: [Function: bound setBaseURL],
    setHeader: [Function: bound setHeader],
    setToken: [Function: bound setToken],
    onRequest: [Function: bound onRequest],
    onResponse: [Function: bound onResponse],
    onRequestError: [Function: bound onRequestError],
    onResponseError: [Function: bound onResponseError],
    onError: [Function: bound onError],
    create: [Function: bound create],
    '$request': [Function: bound ],
    '$delete': [Function: bound ],
    '$get': [Function: bound ],
    '$head': [Function: bound ],
    '$options': [Function: bound ],
    '$post': [Function: bound ],
    '$put': [Function: bound ],
    '$patch': [Function: bound ]
  }
}

That's a lot of information! To be honest at this stage it may seem a little over your head, but that is fine the important thing to remember here is that asyncData method takes in as it first parameter the context object which provides information about the application. There are some advanced use case where this becomes useful but for now we're just happy to know its there!

Typically what you will want to do in the asyncData method is similar to what you use the data() function in a typical Vue component. In Vue the data property is function that enable each instance of a component to have its own data. This is typically a hash that consists of a number of properties.

Nuxt extends this asyncData enabling you to get data from Asynchronous resources! Lets go through this with a simple example.

We'll make use of the asyncData to make a call to an external API and get some data to display.

<template>
  <p>The title is: {{ message }}</p>
</template>

<script>
import axios from 'axios'

export default {
    data () {
        return { message: 'Hello World!' }
    },
  async asyncData() {
    const { data } = await axios.get(
      'https://jsonplaceholder.typicode.com/posts/1'
    )
    return { message: data.title }
  },
}
</script>

You can still use the data() object to set up data, but you need to bear in mind some proviso that the data will always merged and the asyncData properties will take precedence. What this means is that if you have properties with the same name that the value from asyncData will always be there.

Let's try a little example, building on from our previous example.

<template>
  <p>The title is: {{ message }}</p>
</template>

<script>
import axios from 'axios'

export default {
  async asyncData() {
    const { data } = await axios.get(
      'https://jsonplaceholder.typicode.com/posts/1'
    )
    return { message: data.title }
  },
  data() {
    return { message: 'Hello World!' }
  },
}
</script>

If you run that example you'll notice that the message property will still have the data from the API call. If we change the code slightly to create two different variables and populate them with different data, we'll see that both are now available in our template.

<template>
  <div>
    <p>The title is: {{ title }}</p>
    <p>The message is: {{ message }}</p>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  async asyncData() {
    const { data } = await axios.get(
      'https://jsonplaceholder.typicode.com/posts/1'
    )
    return { title: data.title }
  },
  data() {
    return { message: 'Hello World!' }
  },
}
</script>

We'll learn more about Data fetching and making further use of the asyncData in future tutorials.

The fetch method

The fetch method is similar to the asyncData method, except that it is called after the created Vue lifecycle hook, i.e. after initiating the component. It can also be used asynchronously.

<template>
  <div>
    <p>The message is: {{ message }}</p>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  data() {
    return { message: '' }
  },
  async fetch() {
    const { data } = await axios.get(
      'https://jsonplaceholder.typicode.com/posts/1'
    )
    this.message = data.title
  },
}
</script>

Another key difference between the fetch and the asyncData functions is that with fetch you must define the properties you want to populate in the data() function.

Since fetch is called after the page component has been initiated, use the this keyword to access the object in the data method.

We'll be digging deeper into the fetch method in later tutorials when we start interacting the Vuex Store

The Head method

The head method is used to set the <meta> tags on a page, we'll be covering this in deeper detail when we start focusing the SEO functionality that Nuxt provides.

The layout property

we have previously discussed layout pages in