Nuxt.js 生成sitemap站點地圖檔案

2023-10-12 18:02:05

Nuxt.js 生成sitemap站點地圖檔案

背景介紹

​ 使用nuxt框架生成靜態檔案支援SEO優化,打包之後需要生成一個 sitemap.xml 檔案方便提交搜尋引擎進行收錄。官網有提供一個外掛sitemap 但是如果是動態路由需要手動一個個設定比較麻煩,無法自動檢索生成。所以自己編寫一個生成 sitemap 模組

準備工作

建立nuxt專案,參考中文官網。安裝JavaScript模板ejs工具

$ npm install ejs

相關網站

sitemap模組

專案根目錄建立 modules 目錄,以及對應檔案,詳細檔案內容放在文末。

├─modules
│  └─robots.ejs // robots模板
│  └─sitemap.js // 站點地圖js
│  └─template.ejs //sitemap 模板

設定 nuxt.config.js

modules 陣列增加以下內容 modules/sitemap 剛才自定義模組,excludes 需要排除的目錄,hostname 站點域名

nuxt.config.js

export default {
  ...省略
  // Modules: https://go.nuxtjs.dev/config-modules
  modules: [
    ...省略,
    ['modules/sitemap',
      {
        excludes: ['_nuxt', 'img'],
        hostname: 'https://www.example.com'
      }
    ],
  ],
}

執行命令生成靜態資源

$npm run generate

開啟專案根目錄下dist(預設輸出路徑),會多出兩個檔案

├─robots.txt
├─sitemap.xml

結果展示

官方範例 modules

編寫自己的模組

模組就是函數。它們可以打包為 npm 模組或直接包含在專案原始碼中。

nuxt.config.js

export default {
  exampleMsg: 'hello',
  modules: [
    // Simple usage
    '~/modules/example',
    // Passing options directly
    ['~/modules/example', { token: '123' }]
  ]
}

modules/example.js

export default function ExampleModule(moduleOptions) {
  console.log(moduleOptions.token) // '123'
  console.log(this.options.exampleMsg) // 'hello'

  this.nuxt.hook('ready', async nuxt => {
    console.log('Nuxt is ready')
  })
}

// REQUIRED if publishing the module as npm package
module.exports.meta = require('./package.json')

1) ModuleOptions

moduleOptionsmodules 這是使用者使用陣列傳遞的物件 。我們可以用它來客製化它的行為。

頂級選項

有時,如果我們可以在註冊模組時使用頂級選項會更方便 nuxt.config.js。這使我們能夠組合多個選項源。

nuxt.config.js

export default {
  modules: [['@nuxtjs/axios', { anotherOption: true }]],

  // axios module is aware of this by using `this.options.axios`
  axios: {
    option1,
    option2
  }
}

2) this.options

this.options:您可以使用此參考直接存取 Nuxt 選項。nuxt.config.js 這是分配有所有預設選項的使用者內容 。它可用於模組之間的共用選項。

模組.js

export default function (moduleOptions) {
  // `options` will contain option1, option2 and anotherOption
  const options = Object.assign({}, this.options.axios, moduleOptions)

  // ...
}

modules檔案

modules/robots.ejs

# robots.txt
User-agent: Baiduspider
Disallow:
User-agent: Sosospider
Disallow:
User-agent: sogou spider
Disallow:
User-agent: YodaoBot
Disallow:
User-agent: Googlebot
Disallow:
User-agent: Bingbot
Disallow:
User-agent: Slurp
Disallow:
User-agent: Teoma
Disallow:
User-agent: ia_archiver
Disallow:
User-agent: twiceler
Disallow:
User-agent: MSNBot
Disallow:
User-agent: Scrubby
Disallow:
User-agent: Robozilla
Disallow:
User-agent: Gigabot
Disallow:
User-agent: googlebot-image
Disallow:
User-agent: googlebot-mobile
Disallow:
User-agent: yahoo-mmcrawler
Disallow:
User-agent: yahoo-blogs/v3.9
Disallow:
User-agent: psbot
Disallow:
Disallow: /bin/
Disallow: /js/
Disallow: /img/
Sitemap: <%= hostname %>/sitemap.xml

modules/sitemap.js

/**
 * @description 生成 sitemap robots 模組
 * @author 方圓百里
 * @time 2023年10月12日
 */

const path = require('path');
const fs = require('fs');
const ejs = require('ejs');
/**
 * @description 獲取當前目錄下載的所有路徑 -同步
 * @author 方圓百里
 *
 * @param {String} dir 檔案路徑
 * @returns {Array} 返回路徑陣列
 */
const loadFiles = (dir) => {
  try {
    const data = fs.readdirSync(dir);
    return data;
  } catch (e) {
    console.error('獲取目錄路徑異常', e)
    return undefined;
  }
}

/**
 * @description 獲取檔案資訊
 * @author 方圓百里
 *
 * @param {String} dir 檔案路徑
 * @returns {Array} 返回路徑陣列
 */
const statFile = (full_path) => {
  try {
    const stat = fs.statSync(full_path);
    stat.path = full_path;
    return stat;
  } catch (e) {
    console.error('獲取目錄路徑異常', e)
    return undefined;
  }
}

/**
 * @description 遞迴處理檔案路徑
 * @author 方圓百里
 *
 * @param {String} dir 檔案路徑
 * @param {String} list 檔案資訊陣列
 * @returns {Array} 返回路徑陣列
 */
const handleFiles = (dir, list = [], excludes) => {
  // 1、載入當前目錄下所有路徑,包含資料夾和檔案
  const data = loadFiles(dir);
  if (data) {
    data.forEach(item => {
      if (!excludes.includes(item)) {
        // 2、拼接絕對路徑
        const absolutePath = path.join(dir, item)
        // 3、獲取檔案基本資訊
        const stat = statFile(absolutePath);
        // 4、如果是檔案,處理基本資訊
        if (stat.isFile()) {
          list.push({
            size: stat.size,
            time: stat.ctime,
            ...path.parse(stat.path)
          })
        } else { // 5、目錄遞迴進行處理
          handleFiles(stat.path, list, excludes);
        }
      }
    })
  }
  return list;
}

/**
 * @description 格式化日期
 * @author 方圓百里
 *
 * @param {Date} date 日期
 * @returns {String} 2023-10-12
 */
const formatYear = (date) => {
  // 獲取年、月和日
  const year = date.getFullYear();
  const month = (date.getMonth() + 1).toString().padStart(2, '0'); // 月份從0開始,需要加1,同時確保兩位數格式
  const day = date.getDate().toString().padStart(2, '0'); // 確保兩位數格式
  // 格式化日期
  return `${year}-${month}-${day}`;
}

/**
 * @description 生成站點地圖
 * @author 方圓百里
 *
 * @param {String} dist 打包後檔案路徑
 * @param {String} hostname 主機名稱
 * @param {Array} excludes 排除路徑
 *
 */
const generateSitemap = (dist, hostname, excludes) => {
  const data = handleFiles(dist, [], excludes)
  const set = new Set();
  for (var i = 0; i < data.length; i++) {
    const f = data[i];
    if (f.ext === '.html') {
      const relative = f.dir.replace(dist, "")
      if (relative) {
        const paths = relative.split(path.sep);
        let loc = hostname;
        for (var x = 1; x < paths.length; x++) {
          loc += "/" + paths[x];
        }
        set.add({
          loc: loc,
          time: formatYear(f.time)
        });
      }
    }
  }
  // 讀取模板檔案
  const template = fs.readFileSync('modules/template.ejs', 'utf-8');
  // 提供模板資料
  const datas = {
    urls: set
  };
  // 使用模板引擎渲染模板
  const renderedContent = ejs.render(template, datas);
  // 寫入生成的檔案
  fs.writeFileSync(path.join(dist, 'sitemap.xml'), renderedContent);
  console.log('sitemap.xml 生成成功!');

  const robotsRendered = ejs.render(fs.readFileSync('modules/robots.ejs', 'utf-8'), {
    hostname
  });
  // 寫入生成的檔案
  fs.writeFileSync(path.join(dist, 'robots.txt'), robotsRendered);
  console.log('robots.txt 生成成功!');
}
export default function ExampleModule(moduleOptions) {
  const dist = this.options.generate?.dir || 'dist'; // 打包輸出路徑
  const hostname = moduleOptions.hostname || 'https://www.example.com'; // 主機名稱
  const excludes = moduleOptions.excludes || ['.nuxt']; // 排除路徑
  console.log('打包輸出路徑:=====>', dist)
  console.log('主機名稱:=====>', hostname)
  console.log('排除路徑:=====>', excludes)

  this.nuxt.hook('generate:done', async generator => {
    // 這將在Nuxt生成頁面之之後呼叫
    console.log('執行 generate 完成')
    generateSitemap(dist, hostname, excludes)

  })
}

// 將模組釋出為npm包
module.exports.meta = require('../package.json')

modules/template.ejs

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
  xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0"
  xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
  xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
  <url>
    <% urls.forEach(function(item) { %>
       <loc><%= item.loc %></loc>
       <lastmod><%= item.time %></lastmod>
       <changefreq>monthly</changefreq>
       <priority>0.8</priority>
    <% }); %>
  </url>
</urlset>