Project Base

Base project documentation — Nuxt 4 Template

Tech Stack

CategoryPackageVersion
FrameworkNuxt4.4.x
LanguageTypeScript6.x
UIVue3.5.x
FontYakuHanJP4.x
SEO - Sitemap@nuxtjs/sitemap8.x
SEO - Robots@nuxtjs/robots6.x
LintingESLint (@nuxt/eslint)10.x
FormattingPrettier3.x

Cấu trúc thư mục

app/
├── assets/css/global.css        # CSS reset & base styles (YakuHanJP font)
├── components/                  # Vue components
├── composables/
│   ├── useOptimizedSrc.ts       # microCMS image optimize (resize, webp)
│   ├── useSeo.ts                # SEO meta composable (title, desc, OG, canonical)
│   └── useJsonLd.ts             # JSON-LD structured data composables
├── layouts/default.vue          # Layout mặc định
├── pages/index.vue              # Trang chủ
├── plugins/
│   └── gtm.client.ts           # GTM delay load (window.load + 2s)
├── types/index.ts               # Shared TypeScript types
├── utils/format.ts              # Utility functions (date, string, url)
└── app.vue                      # Root component

server/
├── api/
│   ├── __sitemap__/urls.ts      # Dynamic sitemap URLs
│   └── example.get.ts           # API route mẫu
└── utils/
    ├── api-handler.ts           # safeEventHandler (try/catch wrapper)
    └── microcms.ts              # microCMS client ($fetch, server-only)

Biến môi trường (.env)

BiếnMô tảVí dụ
SITE_URLURL site cho sitemap & canonical"https://example.com"
TYPE_ROBOTSRobots meta tag"index, follow"
MICRO_CMS_API_KEYmicroCMS API key (server-only)
MICRO_CMS_SERVICE_DOMAINmicroCMS service domain (server-only)"your-service"
GTM_IDGoogle Tag Manager ID"GTM-XXXXXXX"
Lưu ý: Biến MICRO_CMS_* chỉ dùng ở server, không bị expose ra client. Biến public trong runtimeConfig.public expose ra cả client.

Composables

Composables được Nuxt auto-import, dùng trực tiếp trong <script setup> không cần import.

useSeo — SEO meta tags

Set title, description, canonical, Open Graph, Twitter Card, robots cho mỗi page. Mỗi page bắt buộc phải gọi.

useSeo({
  title: 'ページタイトル | サイト名',         // Bắt buộc
  description: '説明文 120〜160文字...',      // Bắt buộc, 120-160 ký tự
  ogImage: '/images/og-page.webp',           // Tối thiểu 1200x630px
  // canonicalUrl: 'https://...',            // Override nếu cần
  // noindex: true,                          // Ẩn khỏi search engine
})
OptionTypeBắt buộcMô tả
titlestringYesTitle page, format: ページ名 | サイト名
descriptionstringYesMeta description, 120-160 ký tự
ogImagestringOG image URL, tối thiểu 1200x630px, format WebP
canonicalUrlstringOverride canonical URL (mặc định tự sinh)
noindexbooleanSet robots noindex, nofollow

useOptimizedSrc — Tối ưu ảnh microCMS

Tự động thêm query params resize, quality, format cho URL ảnh microCMS. URL khác trả nguyên, null/undefined trả /images/default.webp.

const { optimizeSrc } = useOptimizedSrc()

// Default: width=1200, quality=100, format=webp
optimizeSrc(article.thumbnail)

// Custom options
optimizeSrc(article.thumbnail, { width: 600, quality: 80 })

useJsonLd — Structured Data (JSON-LD)

Inject <script type="application/ld+json"> vào head.

Loại pageComposableBắt buộc?
HomepageuseJsonLdOrganization()Yes
Mọi page có breadcrumbuseJsonLdBreadcrumb()Yes
Bài viết / BloguseJsonLdArticle()Yes
Tin tứcuseJsonLdNewsArticle()Yes
Trang FAQuseJsonLdFaq()Yes
// Homepage
useJsonLdOrganization({
  name: 'Site Name',
  url: 'https://example.com',
  logo: '/images/logo.png',
})

// Breadcrumb
useJsonLdBreadcrumb([
  { name: 'ホーム', url: 'https://example.com' },
  { name: 'ブログ', url: 'https://example.com/blog' },
  { name: article.title, url: `https://example.com/blog/${article.id}` },
])

// Article
useJsonLdArticle({
  headline: article.title,
  description: article.excerpt,
  image: article.thumbnail,
  datePublished: article.publishedAt,
  dateModified: article.updatedAt,
  author: { name: 'Author Name' },
  publisher: { name: 'Site Name', logo: '/images/logo.png' },
})

// FAQ
useJsonLdFaq([
  { question: '質問1?', answer: '回答1' },
  { question: '質問2?', answer: '回答2' },
])

Utilities (utils/format.ts)

Tất cả hàm trong utils/ được Nuxt auto-import, dùng trực tiếp trong <template><script setup>.

Date (UTC → JST)

Tất cả hàm date tự động convert UTC sang giờ Nhật Bản (Asia/Tokyo).

HàmOutputLive demo
formatDateDot(date)YYYY.MM.DD2021.07.03
formatDateTimeDot(date)YYYY.MM.DD hh:mm:ss2021.07.04 00:30:00
formatDateJP(date)YYYY年M月D日2021年7月3日

String & URL

HàmMô tảLive demo
truncate(str, max)Cắt chuỗi + ellipsistruncate('hello world', 5)hello...
getDomain(url)Lấy hostname từ URLgetDomain('https://example.com/path')example.com

Plugins

Google Tag Manager (plugins/gtm.client.ts)

GTM delay load: đợi window.load + 2 giây mới inject script → không ảnh hưởng Core Web Vitals.

  • Client-only plugin
  • Để trống GTM_ID = không load GTM
  • Cấu hình trong nuxt.config.tsruntimeConfig.public.GTM_ID: 'GTM-XXXXXXX'

Server-side API

Kiến trúc: Client → Server Route → External API. API key nằm ở server, client không bao giờ thấy.

safeEventHandler (server/utils/api-handler.ts)

Wrapper cho defineEventHandler — bọc toàn bộ logic trong try/catch, server route không bao giờ crash.

// server/api/blogs.get.ts
export default safeEventHandler(async (event) => {
  const { data, error } = await safeMicroCmsGet('/blogs')
  if (error) {
    throw createError({ statusCode: error.statusCode, message: error.message })
  }
  return data
})

microCMS Client (server/utils/microcms.ts)

Dùng $fetch (ofetch) built-in của Nuxt. Retry 2 lần, timeout 10s.

HàmMô tảThrow?
microCmsGet(endpoint, params?)Gọi GET, trả data trực tiếpYes — cần try/catch
safeMicroCmsGet(endpoint, params?)Safe wrapper, trả { data, error }Không bao giờ
microCmsGetList(endpoint, params?)Lấy list với phân trangYes — cần try/catch
// Lấy list (safe)
const { data, error } = await safeMicroCmsGet('/blogs', {
  limit: 10,
  offset: 0,
})

// Lấy single content
const blog = await microCmsGet('/blogs/abc123')

// Gọi API khác (không phải microCMS)
const data = await $fetch('https://api.example.com/data', {
  headers: { Authorization: `Bearer ${useRuntimeConfig().someApiKey}` },
  retry: 2,
})

SEO Rules (BẮT BUỘC)

1. Mỗi page phải gọi useSeo()

  • title: ngắn gọn, chứa keyword, format ページ名 | サイト名
  • description: 120-160 ký tự, mô tả nội dung trang
  • OG image: mỗi page cần có, tối thiểu 1200x630px, format WebP

2. Robots theo môi trường

Môi trườngTYPE_ROBOTSKết quả
Development / Stagingnoindex, nofollowGoogle KHÔNG index
Productionindex, followGoogle index bình thường

3. Canonical URL

Tự động sinh từ SITE_URL + route.path. Override bằng canonicalUrl trong useSeo() khi cần.

4. Sitemap

  • Static pages tự động được thêm bởi @nuxtjs/sitemap
  • Dynamic pages khai báo trong server/api/__sitemap__/urls.ts
  • URL: https://your-site.com/sitemap.xml

5. HTML Lang

Mặc định: lang="ja". Thay đổi trong nuxt.config.tsapp.head.htmlAttrs.lang.

6. Checklist deploy production

  • Kiểm tra robots.txt
  • Kiểm tra sitemap.xml
  • Submit sitemap lên Google Search Console
  • Test structured data tại Rich Results Test
  • Test OG tags tại Open Graph Debugger

Quy tắc gọi API (BẮT BUỘC)

1. Client KHÔNG gọi trực tiếp external API

// SAI — API key bị lộ ra browser
const data = await $fetch('https://xxx.microcms.io/api/v1/blogs', {
  headers: { 'X-MICROCMS-API-KEY': 'secret' },
})

// DUNG — Gọi qua server route
const { data } = await useFetch('/api/blogs')

2. Server route luôn dùng safeEventHandler

// SAI — nếu lỗi sẽ crash
export default defineEventHandler(async () => {
  const data = await microCmsGet('/blogs')
  return data
})

// DUNG — có safe wrapper
export default safeEventHandler(async () => {
  const { data, error } = await safeMicroCmsGet('/blogs')
  if (error) {
    throw createError({ statusCode: error.statusCode, message: error.message })
  }
  return data
})

3. Client dùng useFetch / useAsyncData

// Cách 1: useFetch (recommended, auto SSR hydration)
const { data, error, status } = await useFetch('/api/blogs')

// Cách 2: useAsyncData (cần transform hoặc combine)
const { data } = await useAsyncData('blogs', () =>
  $fetch('/api/blogs', { params: { limit: 10 } })
)

// Cách 3: $fetch trực tiếp (trong event handler, không cần SSR)
const data = await $fetch('/api/blogs')

4. Gọi nhiều API song song

// Client-side
const [blogs, news] = await Promise.all([
  $fetch('/api/blogs'),
  $fetch('/api/news'),
])

// Server-side (dùng safe wrapper)
const [blogsResult, newsResult] = await Promise.allSettled([
  safeMicroCmsGet('/blogs'),
  safeMicroCmsGet('/news'),
])

TypeScript Types (types/index.ts)

// API
interface ApiResponse<T> { data: T; status: number }
interface ApiError { message: string; statusCode: number }

// microCMS
interface MicroCmsListResponse<T> {
  contents: T[]
  totalCount: number
  offset: number
  limit: number
}
interface MicroCmsContent {
  id: string
  createdAt: string
  updatedAt: string
  publishedAt: string
  revisedAt: string
}

// SEO options
interface SeoOptions {
  title: string
  description: string
  ogImage?: string
  canonicalUrl?: string
  noindex?: boolean
}

// Breadcrumb / FAQ
interface BreadcrumbItem { name: string; url: string }
interface FaqItem { question: string; answer: string }

Scripts

yarn dev          # Dev server tại http://localhost:3000
yarn build        # Build production
yarn preview      # Preview bản build
yarn lint         # Kiểm tra lỗi ESLint
yarn lint:fix     # Tự sửa lỗi ESLint
yarn format       # Format code bằng Prettier

Project Base — Nuxt 4 Base Project Template