Nuxt.js Fundamental ตอนที่ 12 - ทำ Workshop เว็บ Portfolio

Published on

เขียนวันที่ : Aug 26, 2020

LastModified on

(อัพเดท : Mar 20, 2022)

Discord

มาเริ่มลงมือทำ Workshop กันดีกว่าครับ หลังจากได้เรียนรู้ Nuxtjs มา 11 ตอนแล้ว สำหรับตอนนี้จะเป็นการนำเอาทุกๆอย่างที่เรียนมาทั้งหมด มาทำ Workshop กันนะครับ โดยอาจจะมีสอดแทรกเนื้อหาบางส่วนลงไปเพิ่มเติมบ้าง และส่วนไหนอ่านไม่เข้าใจ ก็ย้อนกลับไปอ่านตอนเก่าๆ หรือทำความเข้าใจเพิ่มเติมนะครับ

จุดประสงค์ของ Workshop นี้คือ

  • ประยุกต์ใช้ Nuxt Routing
  • ประยุกต์ใช้ Nuxt Content
  • ประยุกต์ใช้ asyncData และการ fetch API
  • ประยุกต์ใช้การทำ Nuxt Authentication
  • ประยุกต์ใช้การทำ SEO และ Meta Tags
  • ประยุกต์ใช้การกำหนด nuxt.config.js เพื่อกำหนด mode ต่างๆ
  • สามารถ deploy Nuxt และเผยแพร่เว็บไซต์ของเราได้

ตัวอย่างหน้าตา Project Workshop ที่เราจะทำวันนี้

Step 1 - สร้างโปรเจ็คด้วย Create Nuxt App

เริ่มสร้างโปรเจ็คใหม่เลยครับ

  • ผมตั้งชื่อว่า nuxt-portfolio
  • ใช้ axios, conten และ auth modules
  • CSS ใช้เป็น bulma ครับ (จริงๆ ให้เห็นภาพการใช้ css เฉยๆ)
npx create-nuxt-app nuxt-portfolio

ก็จะได้แบบนี้ครับ

? Project name: nuxt-portfolio
? Programming language: JavaScript
? Package manager: Npm
? UI framework: Bulma
? Nuxt.js modules: Axios, Content
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: -

เมื่อสร้างโปรเจ็คเสร็จเรียบร้อยแล้ว ก็ทำการ Start development mode แล้วเริ่ม dev กันเลย

cd nuxt-portfolio
npm run dev

Step 2 - กำหนด Routing ต่างๆ

สร้างหน้า Blog index และ Blog Post ครับ

  • pages/blog/index.vue - หน้า Blog List
  • pages/blog/_slug.vue - หน้า Blog Post

ตอนนี้ก็สร้างไฟล์เปล่าๆ ขึ้นมาไว้ก่อน นอกจากนั้น ก็จะมีไฟล์อื่นๆ อีกคือ

  • pages/login.vue - เอาไว้สำหรับหน้า Login
  • pages/profile.vue - เป็นหน้า Profile หน้านี้จะเอาไว้ fetch Github API เพื่อแสดงรายละเอียดของเรา
  • pages/about.vue - หรือหน้าอื่นๆ ที่อยากเพิ่ม (อันนี้แล้วแต่ free style เลยครับ)

เมื่อเรามีหน้าพร้อมแล้ว ต่อไปก็เริ่ม implement ทีละส่วน เริ่มจากหน้า Blog ก่อนเลย

ซึ่ง ทั้ง 2 ไฟล์นี้ เราจะให้มันอ่าน Content โดยใช้ @nuxt/content ครับ

Step 3 - Nuxt Content

เราใช้ Nuxt Content เพื่อทำ blog post โดยใช้ markdown ในการเขียนบทความ (Nuxt Content ติดตั้งมาพร้อมตอน create-nuxt-app เรียบร้อยแล้ว หากใครไม่ได้เลือกตอนสร้าง ก็ install และเพิ่มใน nuxt.config.js ด้วยนะครับ)

ใน folder content จะเห็นว่ามี hello.md อยู่แล้ว เพราะมัน generate มาใหม่

ผมทำการย้ายไป folder content/blog ที่สร้างมาใหม่ จากนั้นก็ทำการเพิ่มบทความ .md อีก 3-4 บทความ เช่น

---
title: Create blog with Nuxt.js
description: 'ลองสร้างไฟล์ Markdown ด้วย @nuxt/content และทำเป็น blog เพื่อไปแสดงหน้า blog'
createdAt: '2020-08-16T01:58:51+07:00'
---

สวัสดีครับ

บล้อกนี้เขียนด้วย `@nuxt/content` โดยใช้ Markdown

    ```js
    const hello = (name = 'Devahoy') => console.log(`Hello World!, ${name}`)

    hello()
    hello('John Doe')
    ```

ใส่ Link [Devahoy](https://devahoy.com)

Step 4 - แสดงข้อมูล Content

ต่อมาที่ไฟล์ pages/blog/index.vue เพิ่มโค๊ดนี้ลงไป

pages/blog/index.vue
<template>
  <div class="container mt-4">
    <div class="columns is-multiline">
      <div class="column is-4" v-for="post in posts" :key="post.slug">
        <div class="card">
          <div class="card-content">
            <nuxt-link :to="`/blog/${post.slug}`" class="title">{{ post.title }}</nuxt-link>
            <p>{{ post.description }}</p>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    async asyncData({ $content }) {
      const posts = await $content('blog').sortBy('createdAt', 'asc').fetch();

      return { posts };
    }
  };
</script>

<style scoped>
  .card {
    min-height: 250px;
    max-height: 250px;
    padding: 1em;
    overflow: scroll;
  }
</style>

ส่วนสำคัญคือ เราใช้ $content('blog').sortBy('createdAt', 'asc') เพื่อทำการดึง content ทั้งหมดมาแสดง โดยเรียงจากบทความล่าสุด ผ่าน asyncData นั่นเอง และใน template เราก็แค่วน loop แสดงค่า posts ครับ v-for="post in posts" :key="post.slug"

นอกจากนี้จะเห็นว่าผมมีใช้ <nuxt-link> ไปที่ /blog/:slug โดยใช้ post.slug ซึ่งเป็น dynamic route ที่เรากำลังจะทำ

ที่ไฟล์ pages/blog/_slug.vue มีโค๊ดดังนี้

pages/blog/_slug.vue
<template>
  <div class="container">
    <article>
      <h1 class="title">{{ post.title }}</h1>
      <nuxt-content :document="post" />
    </article>
  </div>
</template>

<script>
  export default {
    async asyncData({ $content, params }) {
      const post = await $content('blog', params.slug).fetch();

      return {
        post
      };
    }
  };
</script>

จะเห็นว่าที่ asyncData ผมรับค่า params จากนั้น ใช้ params.slug เป็น args ที่ 2 ส่งไปสำหรับ $content('blog', slug) ครับ มันจะ query ค่ากลับมาเป็น Object ถ้าเจอ (ต่างกับการใช้ $content('blog').where() นะครับ แบบนั้นจะส่งกลับมาเป็น array)

และที่ template ก็ใช้ <nuxt-content> สำหรับแสดงผล data จาก Markdown นั่นเอง

ทดลองเข้าเว็บ http://localhost:3000/blog และลองเลือกคลิ๊กบทความ ก็จะไปหน้ารายละเอียดบทความได้

Step 5 - เพิ่ม Navbar Menu

จะเห็นว่าเรามีเพิ่มหน้าเว็บแล้ว แต่ไม่ได้ link ไปแต่ละหน้า ตอนนี้เข้าโดยพิมพ์ url ก็เลยมาเพิ่ม navbar ให้เว็บดีกว่า โดยผมใช้ default ของ bulma เลยครับ ไม่ได้ปรับอะไรเลย แบบนี้

ผมสร้างไฟล์ components/Navbar.vue ขึ้นมา

components/Navbar.vue
<template>
  <nav class="navbar" role="navigation" aria-label="main navigation">
    <div class="navbar-brand">
      <nuxt-link class="navbar-item" to="/"> Nuxt Portfolio </nuxt-link>

      <a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false">
        <span aria-hidden="true"></span>
        <span aria-hidden="true"></span>
        <span aria-hidden="true"></span>
      </a>
    </div>

    <div class="navbar-menu">
      <div class="navbar-start">
        <nuxt-link class="navbar-item" to="/"> Home </nuxt-link>

        <nuxt-link to="/blog" class="navbar-item"> Blog </nuxt-link>

        <nuxt-link to="/profile" class="navbar-item"> Profile </nuxt-link>
      </div>

      <div class="navbar-end">
        <div class="navbar-item">
          <div class="buttons">
            <nuxt-link to="/login" class="button is-primary">
              <strong>Login</strong>
            </nuxt-link>
          </div>
        </div>
      </div>
    </div>
  </nav>
</template>

<script>
  export default {};
</script>

จากนั้น ที่ไฟล์ layouts/default.vue ผมก็เพิ่ม Navbar ไป

layouts/default.vue
<template>
  <div>
    <Navbar />
    <Nuxt />
  </div>
</template>

จะเห็นว่าผมไม่ต้อง import Component เลย เพราะว่าเราตั้ง auto import component ที่ไฟล์ nuxt.config.js แล้วครับ

Step 6 - ทำหน้า Profile ดึง API

ต่อมาคือ ทำหน้า Profile ครับ ซึ่งหน้านี้ มีการดึง API จาก Github API ครับ คือ

เราใช้ asyncData เพื่อดึงข้อมูลจาก API มาลองแก้ไขไฟล์ pages/profile.vue ดังนี้

pages/profile.vue
<template>
  <div class="has-text-centered container">
    <h2 class="title">My Profile</h2>

    <img :src="user.avatar_url" class="avatar" />

    <p>Name : {{ user.name }}</p>
    <p>Location : {{ user.location }}</p>

    <div class="columns is-multiline my-4">
      <div class="column is-3" v-for="repo in repos" :key="repo.id">
        <div class="card">
          <div class="card-content">
            <a :href="repo.html_url" target="_blank" rel="noopener noreferrer ">{{ repo.name }}</a>
            <p>Star : {{ repo.stargazers_count }}</p>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    async asyncData({ $axios }) {
      const [user, repos] = await Promise.all([
        $axios.$get('https://api.github.com/users/phonbopit'),
        $axios.$get('https://api.github.com/users/phonbopit/repos?sort=pushed&per_page=100')
      ]);

      return { user, repos };
    }
  };
</script>

<style scoped>
  .avatar {
    width: 80px;
  }
</style>

Step 7 - เพิ่ม Authentication หน้า Profile

เมื่อได้หน้า Profile แล้ว ทีนี้ทุกครั้งที่เข้าหน้านี้ มันก็จะไปดึง API มาแสดงผล ทีนี้ถ้าเราอยากใส่ Auth ให้มัน

เราใช้ nuxtjs/auth ครับ รายละเอียดเพิ่มเติม ลองไปอ่าน ตอนที่ 8 - ทำระบบ Authentication ด้วย Nuxt.js เพิ่มเติมได้ครับ

ติดตั้ง Nuxtjs Auth

npm install @nuxtjs/auth

จากนั้นเพิ่มค่าใน nuxt.config.js เพื่อกำหนด module และ strategy

nuxt.config.js
  modules: [
    // Doc: https://github.com/nuxt-community/modules/tree/master/packages/bulma
    '@nuxtjs/bulma',
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    // Doc: https://github.com/nuxt/content
    '@nuxt/content',
    '@nuxtjs/auth'
  ],

    auth: {
    strategies: {
      local: {
        endpoints: {
          login: { url: 'login', method: 'post', propertyName: 'data.token' },
          user: { url: 'me', method: 'get', propertyName: 'data.user' },
          logout: false
        }
      }
    }
  }

ต่อมาทำการ enable Vuex ด้วย ถ้าเราไม่ใช้ Vuex ง่ายสุดคือสร้าง store ขึ้นมาตัวนึง (เพราะตัว Nuxt Auth จะใช้ store.state.auth ในการเก็บข้อมูลนั่นเอง)

สร้างไฟล์ store/index.js

store/index.js
export const state = () => ({
  title: 'Nuxt.js Fundamental by DEVAHOY'
});

ต่อมาสร้างไฟล์ server.js ขึ้นมา (เราจะใช้เป็น API Server กันครับ)

server.js
const express = require('express');
const cors = require('cors');
const app = express();
const router = express.Router();
app.use(cors());
app.use(express.json());
const user = {
  id: 1,
  username: 'john',
  email: 'john@doe.com',
  name: 'John Doe'
};
router.get('/me', (req, res) => {
  return res.json({
    data: {
      user
    }
  });
});
router.post('/login', (req, res) => {
  const { email, password } = req.body;
  // query db.
  if (email === 'admin@admin.com' && password === '123456') {
    return res.json({
      data: {
        user,
        token: 'THIS_IS_TOKEN'
      }
    });
  } else {
    return res.status(401).json({
      message: 'Invalid Password'
    });
  }
});
app.use('/api', router);
app.listen(12345, () => {
  console.log('Mock API start on port 12345');
});

ทำการติดตั้ง express และ cors

npm install express cors

จากนั้น Start server ขึ้นมาเลยครับ (มันจะรันอีก port นึง)

node server.js

ตัว Server API เป็นแค่ mock นะครับ เพื่อให้เห็น flow การทำงานเฉยๆครับ ไม่มีทั้งการ query database การ compare password hash การ sign JWT Token หรืออะไรทั้งนั้น เราข้าม process นั้นๆ เน้นแค่ request และ response ที่จะได้รับครับ

Step 8 - ทำหน้า Login

เมื่อมี API Server แล้ว ต่อมาเราก็มาทำหน้า Login เพื่อเข้าสู่ระบบกันดีกว่า โดย flow ของมันคือ

  1. User กรอกข้อมูล Login แล้วกด Submit
  2. ข้อมูลถูกส่งไป API ด้วย axios แบบ HTTP POST ส่ง username และ password ไป
  3. Server รับข้อมูล (จริงๆ ต้อง query db, compare hash) แต่เราจะข้ามขั้น สมมติว่า username และ password ถูก เราก็จะ sign token (อาจจะเป็น JWT Token นะครับ) กลับมา
  4. เมื่อ response HTTP 200 ตัว Nuxt Auth มันก็จะเซฟ token ที่ได้ไว้ใน localStorage ให้เราเอง (กำหนด name หรือมี prefix ได้)
  5. ถ้าหน้าไหนที่มัน require auth คือต้อง login ตัว Nuxt Auth มันก็จะ request ไปที่ /me (ที่เรากำหนด endpoints.user ไว้ใน strategy) ถ้า response 200 คือปกติ ถ้าเป็นอย่างอื่นคือ unauthorized

Flow คร่าวๆ ก็ประมาณนี้

มาทำหน้า UI กัน ที่ไฟล์ pages/login.vue

pages/login.vue
<template>
  <div class="container">
    <form class="form" @submit="onSubmit">
      <h3 class="title">Log In</h3>
      <div class="field">
        <p class="control">
          <input v-model="email" class="input" type="email" placeholder="Email" />
        </p>
      </div>
      <div class="field">
        <p class="control">
          <input v-model="password" class="input" type="password" placeholder="Password" />
        </p>
      </div>

      <div class="field">
        <button type="submit" class="button is-primary is-fullwidth">Login</button>
      </div>

      <div class="field">
        <p v-if="error" class="notification is-danger">{{ error.message }}</p>
      </div>
    </form>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        email: '',
        password: '',
        error: null
      };
    },
    methods: {
      async onSubmit(e) {
        e.preventDefault();

        const payload = {
          data: {
            email: this.email,
            password: this.password
          }
        };

        try {
          await this.$auth.loginWith('local', payload);
          this.$router.push('/');
        } catch (error) {
          this.error = error;
        }
      }
    }
  };
</script>

<style scoped>
  .container {
    margin: 0 auto;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    text-align: center;
  }

  .form {
    width: 240px;
  }
</style>

เวลาที่ User submit เราจะเรียก auth login ด้วย $auth.loginWith() นั่นเอง

this.$auth.loginWith('local', payload);

Step 9 - Middleware

ทีนี้ถ้าเรา Login Success เราจะมีค่า store.state.auth.loggedIn เป็น true และมีค่า store.state.auth.user ที่ได้จากตอน login และ request /me นั่นเอง

ต่อมาก็ทำการกำหนด ว่าหน้าไหน ที่เราจะให้มัน required auth คือต้องเข้าสู่ระบบก่อนเท่านั้น

  • /blog/:slug - หน้ารายละเอียดบล็อก เราจะบังคับให้ต้อง Login ก่อน
  • /profile - หน้า Profile ก็เช่นกัน ถ้าไม่ได้ login จะเข้าดูไม่ได้

ฉะนั้นก็แค่เพิ่ม middleware: 'auth' ลงไปใน Page ที่เราต้องการ

export default { middleware: 'auth' }

ต่อมาเราเพิ่ม Middleware ตัวนึงเพื่อกันไม่ให้มัน render หน้า /login ถ้าเรา login เข้าระบบแล้ว สร้างไฟล์ middlware/isLoggedIn.js

export default function ({ store, redirect }) {
  if (store.state.isLoggedIn) {
    return redirect('/');
  }
}

และก็ใส่ไว้ในหน้า pages/login ซะ

export default {
  middleware: 'isLoggedIn`
}

ต่อมาปัญหาเรื่องหน้า /profile นิดนึง เราเพิ่มส่วนนี้ลงไปใน asyncData ซะ ที่ไฟล์ pages/profile.vue

$axios.setHeader('Authorization', null);

เนื่องจากใช้ร่วมกับ Nuxt Auth ทุกๆ ครั้งที่เรียก $axios มันจะส่ง headers.authorization ไปด้วย ทำให้เราต้อง ลบ header ก่อน เวลา request ไป Github API ครับ

Step 10 - เพิ่มหน้า Photos

เราลองเพิ่มหน้าเพิ่มดีกว่า ในการดึงข้อมูล API มี 2 หน้าคือ

  • /photos - แสดงรูปทั้งหมด
  • /photos/:id - แสดงรูปตาม id ของรูป

โดย API เราก็ใช้แบบเดียวกันกับ ตอนที่ 6 - การดึงข้อมูลจาก APIs คือเว็บ https://picsum.photos/

รวมถึงไฟล์ pages/photos/index.vue และ pages/photos/_id.vue ก็แบบเดียวกันเลย

ขั้นตอนนี้ ผมอยากให้ผู้อ่าน ลองเพิ่มเองกันดูนะครับ หลักการเหมือนกับการใช้ asyncData ในหน้า pages/profile.vue เลย

เมื่อมีหน้าเพิ่ม เราก็ต้องไปเพิ่ม link ใน Navbar ให้มันด้วย ก็แก้ไข components/Navbar.vue ซะหน่อย

components/Navbar.vue
<div class="navbar-start">
  <nuxt-link class="navbar-item" to="/"> Home </nuxt-link>

  <nuxt-link to="/blog" class="navbar-item"> Blog </nuxt-link>

  <nuxt-link to="/photos" class="navbar-item"> Photos </nuxt-link>

  <nuxt-link to="/profile" class="navbar-item"> Profile </nuxt-link>
</div>

Step 11 - ปรับ Navbar Toggle บน Mobile

เมื่อใช้ Bulma มันก็จะรองรับ Responsive Design อยู่แล้ว เมื่อเราเปิดบนมือถือ หรือหน้าจอขนาดเล็ก ตรง Navbar menu เรามันก็จะเปลี่ยนเป็น hamburger menu ให้เราเอง ทีนี้ เวลาใช้ Bulma เวลากด มันจะ trigger class is-active โดยใช้ JavaScript แต่ใน Nuxt.js เราจะทำยังไงนะ?

วิธีก็ไม่ยาก เรากำหนด state ให้มันก่อน ที่ไฟล์ components/Navbar.vue ดังนี้

export default { data() { return { showNav: false } } }

ใน data มี showNav เป็น false ไว้ก่อน แล้วเวลา menu ถูกคลิก เราก็ค่อยเปลี่ยนเป็น true ก็แก้ไข template เป็นแบบนี้

<a
  role="button"
  class="navbar-burger burger"
  aria-label="menu"
  aria-expanded="false"
+ @click="showNav = !showNav"
>
  <span aria-hidden="true"></span>
  <span aria-hidden="true"></span>
  <span aria-hidden="true"></span>
</a>

ทีนี้ทุกครั้งที่ click มันก็จะ toggle true/false ละ เราก็แค่เอามาเป็นเงื่อนไขในการ show class is-active แบบนี้

<a
  role="button"
  class="navbar-burger burger"
  aria-label="menu"
  aria-expanded="false"
  @click="showNav = !showNav"
+ :class="{ 'is-active': showNav }"
>
  <span aria-hidden="true"></span>
  <span aria-hidden="true"></span>
  <span aria-hidden="true"></span>
</a>

+ <div class="navbar-menu" :class="{ 'is-active': showNav }">
  <div class="navbar-start">
    <nuxt-link class="navbar-item" to="/">
      Home
    </nuxt-link>

    <nuxt-link to="/blog" class="navbar-item">
      Blog
    </nuxt-link>

    <nuxt-link to="/profile" class="navbar-item">
      Profile
    </nuxt-link>
  </div>
</div>

Step 12 - ปรับ SEO และ Meta Tags

ต่อมาเรื่องการปรับ SEO เราจะกำหนด Meta tag global ไว้ที่ไฟล์ nuxt.config.js และกำหนด Title และ description ในแต่ละ page รวมถึงหน้า Profile อาจจะให้เป็น dynamic title และ description ด้วยคครับ

ไฟล์ nuxt.config.js เพิ่มไปเลย

nuxt.config.js
head: {
  titleTemplate: 'Nuxt.js Fundamental - %s',
  title: 'Nuxt.js Fundamental',
  meta: [
    { charset: 'utf-8' },
    { name: 'viewport', content: 'width=device-width, initial-scale=1' },
    { hid: 'description', name: 'description', content: 'Meta description' }
  ]
}

ต่อมา ในแต่ละ Pages เราก็กำหนด head() ให้มันซะ เช่นหน้า pages/index.vue

pages/index.vue
export default {
  head() {
    return {
      title: 'Nuxt Portfolio',
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: 'Nuxt.js Workshop - Building Portfolio with Nuxt.js'
        }
      ]
    };
  }
};

หรืออย่างหน้า Profile ก็ใช้ response จาก API มาเป็น title และ description แบบนี้

export default {
  head() {
    return {
      title: 'Nuxt Portfolio',
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: 'Nuxt.js Workshop - Building Portfolio with Nuxt.js'
        }
      ]
    }
  },
  async asyncData({ $axios }) {
    ...

    const title = `Profile of ${user.name}`;
    const description = `Profile from Github API for ${user.name}`
    return { user, repos, title, description }
  }
}

เราก็จะได้ Dynamic meta tag ตามที่เราต้องการแล้ว

Step 13 - เปลี่ยนมาใช้ Interal API

ตอนนี้เราใช้ API ที่รัน Server แยก เราอยากจะเปลี่ยนไปใช้ Internal API จะได้ไม่ต้องรันหลาย Server ครับ วิธีการก็แค่เพิ่ม serverMiddlware ใน nuxt.config.js ซะ

export default {
  serverMiddleware: ['~/api/auth.js']
};

ต่อมา สร้างไฟล์ api/auth.js และเอาโค๊ด server.js มาใช้ได้เลย

ปรับแก้นิดหน่อย เป็นแบบนี้

api/auth.js
const express = require('express');
const cors = require('cors');
const app = express();
const router = express.Router();
app.use(cors());
app.use(express.json());
const user = {
  id: 1,
  username: 'john',
  email: 'john@doe.com',
  name: 'John Doe'
};
router.get('/me', (req, res) => {
  return res.json({
    data: {
      user
    }
  });
});
router.post('/login', (req, res) => {
  const { email, password } = req.body;
  // query db.
  if (email === 'admin@admin.com' && password === '123456') {
    return res.json({
      data: {
        user,
        token: 'THIS_IS_TOKEN'
      }
    });
  } else {
    return res.status(401).json({
      message: 'Invalid Password'
    });
  }
});
app.use(router);
module.exports = {
  path: '/api',
  handler: app
};

ดูเพิ่มเติม ตอนที่ 10 - การทำ Internal API และ Middlware ครับ

สุดท้ายปรับแก้ baseURL ของ axios ใน nuxt.config.js เป็น Server เดียวกัน เช่น http://localhost:3000/api

nuxt.config.js
axios: {
  baseURL: 'http://localhost:3000/api';
}

เรียบร้อยครับ!

Step 14 - Deploy ไป Heroku

สุดท้ายครับ การ Deploy เราจะใช้ Heroku กันเนื่องจากว่ามีการใช้ Internal Middleware ซึ่ง หากใครมี Server หรือ API แยกอยู่แล้ว เราสามารถ Deploy Nuxt.js ด้วย Netlify หรือ Github Pages ได้นะครับ ลองดู ตอนที่ 9 - การ Deploy Nuxt.js ครับ

สร้าง Create new App ในหน้า Heroku Dashboard เรียบร้อย

ให้เรามาที่ folder ของเรา จากนั้นทำการ init และ push ไป Heroku Server

heroku git:remote -a <YOUR_APP_NAME>

git add .
git commit -am "deploy to heroku"
git push heroku master

เราจะได้ domain ที่ Heroku generate ให้ ลองเข้าเว็บดู จะไม่สามารถเข้าได้ เพราะ Heroku ไม่เจอ port และ hostname ครับ ต้องไปกำหนด environment variable ใน Setting -> Config Vars ครับ

  • HOST ใช้เป็น 0.0.0.0
  • NODE_ENV ใช้เป็น production

เราก็จะได้เว็บที่รันบน Heroku แล้ว แต่ติดปัญหานิดนึงตรง api มันไม่สามารถเข้าถึงได้ด้วย http://localhost:3000/api เราต้องเปลี่ยนเป็น Url ของ Heroku ครับ (ดูจากหน้า Settings -> Domains)

axios: {
  baseURL: 'https://your-heroku-app.herokuapp.com/api';
}

Congratulations! 🎉🎉🎉

สรุป

หวังว่าบทความ Nuxt.js Fundamental ทั้ง 12 ตอน และ Workshop นี้จะเป็นประโยชน์สำหรับเพื่อนๆ พี่ๆ น้องๆ ที่สนใจ หรือกำลังศึกษา Nuxt.js นะครับ จริงๆ ก็รวมถึงคนที่ยังไม่เคยเขียน และอยากลองเขียน Nuxt.js หรือ Vue.js ด้วย เผื่อจะเห็นไอเดีย แนวทางในการเขียน และนำสิ่งที่ได้เรียนรู้ไปประยุกต์ใช้นะครับ

หากเนื้อหาส่วนไหน มีข้อผิดพลาด ตรงไหนอธิบายไม่เคลียร์ สามารถติชมกันได้นะครับ

ขอบคุณที่ติดตามอ่านครับ และหากใครชื่นชอบ ก็อย่าลืมแชร์ อย่าลืมบอกต่อเพื่อนๆ ด้วยนะครับ แล้วพบกัน Tutorial หน้าครับ

Happy Coding ♥️

Buy Me A Coffee
Discord