DEV Community

Dennis Frijlink
Dennis Frijlink

Posted on

Nuxt.js Single Page Application Boilerplate

A boilerplate for single page applications based on the Vue.js Framework, Nuxt.js. Check repository: https://github.com/dennisfrijlink/nuxt-spa-boilerplate

logo of Nuxt Single Page Application Repository

Single Page Application Boilerplate - Nuxt.js

A boilerplate for single page applications based on the Vue.js Framework, Nuxt.js

🧐 What's inside

✨ Quick start

  1. Clone this repository.

    git clone https://github.com/dennisfrijlink/nuxt-spa-boilerplate.git
    
  2. Start developing.

    Navigate into your new site’s directory and start it up.

    cd nuxt-spa-boilerplate/
    npm install
    npm run dev
    
  3. Running!

    Your site is now running at http://localhost:3000!

  4. Generate for deploy

    Generate a static project that will be located in the dist folder:

    $ npm run generate
    

βš™οΈ What is a Single Page Application

A single-page application (SPA) is a web application or website that interacts with the user by dynamically rewriting the current web page with new data from the web server, instead of the default method of the browser loading entire new pages.

In a SPA, all necessary HTML, JavaScript, and CSS code is either retrieved by the browser with a single page load, or the appropriate resources are dynamically loaded and added to the page as necessary, usually in response to user actions. The page does not reload at any point in the process, nor does it transfer control to another page, although the location hash or the HTML5 History API can be used to provide the perception and navigability of separate logical pages in the application.

Lifecycle of Single Page Application

πŸ—ΊοΈ Nuxt Router

Nuxt.js automatically generates the vue-router configuration for you, based on your provided Vue files inside the pages directory. That means you never have to write a router config again! Nuxt.js also gives you automatic code-splitting for all your routes.

To navigate between pages of your app, you should use the NuxtLink component.
For all links to pages within your site, use <NuxtLink>. If you have links to other websites you should use the <a> tag. See below for an example:

<template>
  <main>
    <h1>Home page</h1>
    <NuxtLink to="/about">
      About (internal link that belongs to the Nuxt App)
    </NuxtLink>
    <a href="https://nuxtjs.org">External Link to another page</a>
  </main>
</template>
Enter fullscreen mode Exit fullscreen mode

There a three router modes "hash" | "history" | "abstract":

  • hash: uses the URL hash for routing. Works in all Vue-supported browsers, including those that do not support HTML5 History API.

    • history: requires HTML5 History API and server config. See HTML5 History Mode.
    • abstract: works in all JavaScript environments, e.g. server-side with Node.js. The router will automatically be forced into this mode if no browser API is present.

For example:

// nuxt.config.js

export default {
  router: {
    mode: 'hash'
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ” Data Fetching

Nuxt.js supports traditional Vue patterns for loading data in your client-side app, such as fetching data in a component's mounted() hook.
Nuxt has two hooks for asynchronous data loading:

  • The fetch hook (Nuxt 2.12+). This hook can be placed on any component, and provides shortcuts for rendering loading states (during client-side rendering) and errors.
  • The asyncData hook. This hook can only be placed on page components. Unlike fetch, this hook does not display a loading placeholder during client-side rendering: instead, this hook blocks route navigation until it is resolved, displaying a page error if it fails.

For example:

<template>
  <p v-if="$fetchState.pending">Fetching mountains...</p>
  <p v-else-if="$fetchState.error">An error occurred :(</p>
  <div v-else>
    <h1>Nuxt Mountains</h1>
    <ul>
      <li v-for="mountain of mountains">{{ mountain.title }}</li>
    </ul>
    <button @click="$fetch">Refresh</button>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        mountains: []
      }
    },
    async fetch() {
      this.mountains = await fetch(
        'https://api.nuxtjs.dev/mountains'
      ).then(res => res.json())
    }
  }
</script>
Enter fullscreen mode Exit fullscreen mode

When using the nuxt/http library you can define the baseURL in the nuxt.config.js:

// nuxt.config.js

export default {
  modules: [
    ['@nuxt/http', {
      baseURL: 'https://api.nuxtjs.dev/'
    }]
  ]
}
Enter fullscreen mode Exit fullscreen mode

Now you can use the url of the API in all your pages and components without repeating the base URL:

<!-- pages/index.vue -->

<template>
  <div>
    <h1>{{ mountain.slug }}</h1>
    <img :src="mountain.image" :alt="mountain.slug">
  </div>
</template>

<script>
  export default {
    name: 'index', 
    async asyncData({ $http }) {
      const mountain = await $http.$get('/mountains/aconcagua') // https://api.nuxtjs.dev/mountains/aconcagua
      return { mountain }
    }
  }
</script>
Enter fullscreen mode Exit fullscreen mode

πŸ“± Breakpoints mobile first

The scss folder located in ./assets/scss/ contains two files to make it easier for web developers to prototype, build, scale, and maintain CSS for responsive websites:

SCSS Files

assets
β”‚
└─── scss
    β”‚
    └─── _mixins.scss
    β”‚
    └─── breakpoints.scss
Enter fullscreen mode Exit fullscreen mode

Building responsive websites is a must-have skill for front-end developers today, so we've made the breakpoints mobile first. They are all defined with a @media (min-width: so that the main css you write is based on mobile screens.

// breakpoints.scss


/* Small (sm) */
$screen-sm-min: 640px;

/* Medium (md) */
$screen-md-min: 768px;

/* Large (lg) */
$screen-lg-min: 1024px;

/* Extra Large (xl) */
$screen-xl-min: 1280px;

/* Fluid */
$screen-fluid-min: 1536px;
```
{% endraw %}
{% raw %}`

Now it’s time to create the most important element – mixins:
`{% endraw %}
{% raw %}
```scss
// _mixins.scss


// Small devices
@mixin  sm {
  @media (min-width: #{$screen-sm-min}) {
    @content;
  } 
}

// Medium devices
@mixin  md {
  @media (min-width: #{$screen-md-min}) {
    @content;
  } 
}

// Large devices
@mixin  lg {
  @media (min-width: #{$screen-lg-min}) {
    @content;
  } 
}

// Extra large devices
@mixin  xl {
  @media (min-width: #{$screen-xl-min}) {
    @content;
  } 
}

// Extra large devices
@mixin  fluid {
  @media (min-width: #{$screen-fluid-min}) {
    @content;
  } 
}
```
{% endraw %}
{% raw %}`

I always build my websites in a mobile-first approach, so I don’t need to define the smallest screen size (xs – extra small) and I write my SCSS code first for the smallest devices and next for the largest. Sometimes we also need to define some styles beyond the rigidly defined breakpoints. Let’s add one more mixin – I called it β€œrwd”:
`{% endraw %}
{% raw %}
```scss
// _mixins.scss


// Custom devices
@mixin rwd($screen) {
  @media (min-width: $screen+'px') {
    @content;
  }
}
```
{% endraw %}
{% raw %}`
As a parameter `{% endraw %}$screen{% raw %}` we can put any screen size.

### For Example
`{% endraw %}
{% raw %}
```css
.container {
    padding: 0 15px;

    /* 576px window width and more */
    @include sm {
        padding: 0 20px;
    }

    /* 992px window width and more */
    @include lg {
        margin-left: auto;
        margin-right: auto;
        max-width: 1100px;
    }

    /* 1400px window width and more */
    @include rwd(1400) {
        margin-bottom: 20px;
        margin-top: 20px;
    }
}
```
{% endraw %}
{% raw %}`

## πŸ’¬ Nuxt-i18n
Nuxt-I18n is the Vue.js internationalization plugin optimized for using in Nuxt.js. The configuration of the languages is defined in the `{% endraw %}{% raw %}`nuxt.config.js`{% endraw %}{% raw %}` file:
`{% endraw %}{% raw %}``js
// nuxt.config.js

{
  modules: [
    'nuxt-i18n'
  ],
  i18n: {
    locales: [
      {
        code: 'en',
        iso: 'en-US',
        name: 'English',
      },
      {
        code: 'nl',
        iso: 'nl-NL',
        name: 'Dutch',
      }
    ],
    defaultLocale: 'en',
    vueI18n: {
      fallbackLocale: 'en',
      messages: {
        en: require('./locales/en.json'),
        nl: require('./locales/nl.json')
      }
    }
  }
}
``{% endraw %}{% raw %}`
The locales are located in the `{% endraw %}{% raw %}`~/locales`{% endraw %}{% raw %}` folder:
`{% endraw %}
{% raw %}
```
locales
β”‚
└─── en.json
β”‚
└─── nl.json
```
{% endraw %}
{% raw %}`
`{% endraw %}
{% raw %}
```json
// nl.json

{
  "welcome": "Een boilerplate voor single page application gebasserd op Nuxt.js"
}
```
{% endraw %}
{% raw %}`
`{% endraw %}
{% raw %}
```json
// en.json

{
  "welcome": "A boilerplate for single page applications based on Nuxt.js"
}
```
{% endraw %}
{% raw %}`

When rendering internal links in your app using `{% endraw %}<nuxt-link>{% raw %}`, you need to get proper URLs for the current locale. To do this, **nuxt-i18n** registers a global mixin that provides some helper functions:
-   `{% endraw %}localePath{% raw %}`  – Returns the localized URL for a given page. The first parameter can be either the path or name of the route or an object for more complex routes. A locale code can be passed as the second parameter to generate a link for a specific language:
`{% endraw %}{% raw %}`` vue
<nuxt-link :to="localePath('/')">{{ $t('home') }}</nuxt-link>
<nuxt-link :to="localePath('index', 'en')">Homepage in English</nuxt-link>
<nuxt-link :to="localePath('/app/profile')">Route by path to: {{ $t('Profile') }}</nuxt-link>
<nuxt-link :to="localePath('app-profile')">Route by name to: {{ $t('Profile') }}</nuxt-link>
<nuxt-link
  :to="localePath({ name: 'category-slug', params: { slug: category.slug } })">
  {{ category.title }}
</nuxt-link>
<!-- It's also allowed to omit 'name' and 'path'. -->
<nuxt-link :to="localePath({ params: { slug: 'ball' } })">{{ category.title }}</nuxt-link>
```
-   `switchLocalePath`  – Returns a link to the current page in another language:
``` vue
<nuxt-link :to="switchLocalePath('en')">English</nuxt-link>
<nuxt-link :to="switchLocalePath('fr')">Français</nuxt-link>
```
Template:
```html 
<p>{{ $t('welcome') }}</p>
``` 
Output:
```html 
<p>A boilerplate for single page applications based on Nuxt.js</p>
``` 
## πŸ—› Fonts
There are two standard declarations for the font types:
`

```css
/* standard declrations */
h1,h2,h3,h4,h5,h6 {
  font-family: 'DM sans';
}
body {
  font-family: 'Arial';
}
```

`

These font-families are defined in the same file `font.css`:
`

```css
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('~assets/fonts/DM-Sans/DMSans-Regular.ttf') format('truetype');
}
```

`
If you wanna use a different font family in a specified component you can use another specified font in from the `font.css` inside the `<style lang="scss" scoped></style>` of the `.vue` component

## πŸŒ— Dark & Light theme
πŸŒ‘ Dark and πŸŒ• Light mode with auto detection made easy with the plugin `nuxt/color-mode`. 

### Note
If you don't need a dark/sepia/light mode you can always disable it by commenting this line in `{% endraw %}nuxt.config.js{% raw %}`:
`{% endraw %}
{% raw %}
```js
modules: [
  '@nuxtjs/color-mode'
],
```
{% endraw %}
{% raw %}`

### Theme file
The main theme file, located in `{% endraw %}css/theme.css{% raw %}` contains all css rules specific for `{% endraw %}nuxtjs/color-mode{% raw %}`. In the `{% endraw %}theme.css{% raw %}` you declare all color variables per theme. So for example:

`{% endraw %}
{% raw %}
```css
:root {
  --bg-color: #ffffff;
}

.dark-mode {
  --bg-color: #21252b;
}

body {
  background-color: var(--bg-color);
  transition: background-color .3s;
}
```
{% endraw %}
{% raw %}`

### Important
We use [PurgeCSS](https://github.com/FullHuman/purgecss) for removing unused CSS selectors to optimize the performance of the web application. But PurgeCSS will delete all css rules of the theme(s) that are not selected.
To resolve this issue you'll have to add the theme classes in to the white list of PurgeCSS. So for example:
`

```js
//nuxt.config.js

purgeCSS: {
  whiteList: () =>['dark-mode']
},
```

`
Now PurgeCSS will ignore those classes by removing the unused CSS selectors
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
adamaslan profile image
Adam Aslan

Looks cool! Any suggestions for more advanced displaying of images with zoom most likely in a lightbox or modal?