🤔

Vite で Pug + Sass の静的Web開発環境を整えた

2023/11/23に公開

Vite で静的環境を作れないかなと調べてみたところ出来そうな先人の知恵があったので、個人的な備忘録としてまとめます。

経緯

静的環境は個人的に Pug + Sass が使いやすかったので、今回もその環境を整えようと思いまして。
以前といっても何年前以上の話で、当時は gulp と browserSync とか webpack で作っていましたが、今は何がいいのだろうと探すところから始まりました。
(VueやNuxtなど色々渡り歩いてきましたが、静的サイト用の環境が欲しくなったのが経緯)

いい感じのものが Vite でした。(ヴァイトかと思ったらヴィートと読むらしい)

なので今回は、Vite + Pug + Sass で静的環境を作ってみます。

環境構築前に

※今回の開発環境にはこちらの記事を参考にさせていただいています。先人様に感謝です。
https://zenn.dev/yend724/articles/20220408-tfq16buha8ctdzp7

この記事では、以降上記の記事を作成した上で、個人的に使いやすく変更した点を記載しています。
Viteで Pug + Sass の環境を整えるのであれば先人様の記事から参照ください。

また、 vite-plugin-pug-static というPugをHTMLとして出力するプラグインもあります。
https://www.npmjs.com/package/@macropygia/vite-plugin-pug-static

個人的にgulpで環境構築する際にプラグインを多用して痛い思いをしたので、今回は目に見える形で実装されていた先人様の環境を採用しました。

追加した点

Sassも追加する

Pugは先人様の環境でできていましたのでSassを後付けしました。

yarn add -D sass

yarn create vite していると、 /src/style.css がありますので
/src/style.sass に変更します。
Scssならコードを書き換える必要はないですが、Sassの場合は {} や ; を削除しておきましょう。

Styleをimportしているのが /src/main.ts です。

src/main.ts
- import './style.css'
+ import './style.sass'

これだけでSassを導入できるのは楽でいいですよね。

index.pug以外のPugもビルドする

src/index.pug 以外のPug、 src/test.pugsrc/contact.pug とかあまり使うことがあるかわからないものですが、時と場合によっては必要になるものと思います。
また、 src/test/index.pug とディレクトリ階層を参照がうまく動作していなかったのでこれも合わせて対応します。

Pugファイルを検索するためにglobuleを使っていきます。
globuleはワイルドカードでファイルを検索してくれます。

yarn add -D globule

vite.config.jsで src/**/*.pug を検索を追加します。

vite.config.js
import { resolve } from "path";
import { defineConfig } from "vite";
import vitePluginPug from "./plugins/vite-plugin-pug";

+ import globule from "globule"

+ const htmlFiles = globule.find('src/**/*.pug', {
+   ignore: [
+     'src/**/_*.pug'
+   ]
+ });

export default defineConfig({
  root: "src",
  build: {
    outDir: resolve(__dirname, "dist"),
    rollupOptions: {
-       input: {
-         main: resolve(__dirname, "src", "index.pug"),
-       },
+       input: htmlFiles
    },
  },
  plugins: [
    // options と locals の設定例
    vitePluginPug({
      build: {
        locals: { hoge: "hoge" },
        options: { pretty: true },
      },
      serve: {
        locals: { hoge: "hoge" },
        options: { pretty: true },
      },
    }),
  ],
});

この時、ignoreでinclude用のPugは除外しています。

pugのテンプレート文法の多用とAtomicDesign採用

Pugは閉じタグとかclassとid指定がCSSと同じだったりメリットがありますが、extendsやblockといった文法もかなりのメリットと思います。

個人的に使いやすい形にしたPugファイル構造をまとめます。
ディレクトリ構造にはAtomicDesignを採用。

/src/
  ├─ _includes/
  │ ├─ atoms/ (svgなどCSS Styleが不必要なパーツ)
  │ │ ├─ _icon_xxx.pug
  │ │ └─ _xxxxxx.pug
  │ ├─ molecules/ (リストやCSS Styleが必要な汎用パーツ)
  │ │ ├─ _list_xxx.pug
  │ │ └─ _xxxxxx.pug
  │ ├─ organisms/ (意味を持つブロック)
  │ │ ├─ _about.pug
  │ │ └─ _xxxxxx.pug
  │ └─ _layout.pug (headなど共通要素、extendsで使用)
  ├─ test/
  │ ├─ test.pug
  │ └─ index.pug
  └── index.pug
src/_includes/_layout.pug
- var DOMAIN = "https://xxxx.xxx/";
- var BASE_DOMAIN = "https://xxxx.xxx/";
- var DIR_ASSETS = BASE_DOMAIN + "/assets/";
- var DIR_IMG = DIR_ASSETS + "img/";
block vars
doctype html
html(lang='ja')
  head
    meta(charset='utf-8')
    title #{_title_str}
    meta(http-equiv='X-UA-Compatible', content='IE=edge')
    meta(name='description', content=_desc_str)
    meta(name='keywords', content=_keys_str)
    meta(property="og:title", content=_title_str)
    meta(property="og:type", content="website")
    meta(property="og:description", content=_desc_str)
    meta(property="og:url", content=DOMAIN)
    meta(property="og:image", content=DIR_IMG + "ogp.jpg")
    meta(name="twitter:card", content="summary_large_image")
    meta(name='viewport', content='width=device-width, initial-scale=1.0", user-scalable=yes')
    link(rel='apple-touch-icon', href=DIR_IMG + "favicon.png")
    link(rel='shortcut icon', href=DIR_IMG + 'favicon.png')
    script(src="/main.ts" type="module")
    block link
  body
    .wrapper
      header.header

      main
        .container
          block body

      footer.footer

    block script
src/index.pug
extends _includes/_layout
block vars
  - var _title_str = ''; // タイトルタグ
  - var _desc_str = ''; // descriptions
  - var _keys_str = ''; // keywords

block link

block body

  include _includes/organisms/_about.pug

block script
  //- ページ固有のjs

_layout.pugでは全ページに共通となるhead要素やbody内もheaderやfooterを記載したり、main要素にindex.pugのページごとの要素が流れ込む感じにしています。

index.pugはorganismsのブロック要素などを組み合わせていく使い方を主にしています。
利用規約や簡単で端的なページについては直接コーディングしてしまうこともありますが、どちらにも対応できるようにしています。

どちらも/_includes/のPugファイルを参照して、かつ汎用パーツなどを流用できるようにしています。
仕組み的にはNuxtなどのComponentに近い使い方ができています。

Sassも同じようにしています

PugでAtomicDesignを採用していますが、Sassでも同じように採用しています。
どちらも採用したのには理由があり、NuxtのComponentの構造では、VueファイルにHTMLとCSSが双方とも内蔵しており一括りにできましたが、今回はPugとSassと分けてしまっているためどのファイルでStyleしているのか迷子になりかねないです。
そこで、双方にAtomicDesignを採用してディレクトリ構造とファイル名をPugとSass共に共通化することで解決してみました。

/src/
  ├─ _includes/
  │ ├─ atoms/
  │ │ ├─ _icon_xxx.pug
  │ │ └─ _xxxxxx.pug
  │ ├─ molecules/
  │ │ ├─ _list_xxx.pug
  │ │ └─ _xxxxxx.pug
  │ ├─ organisms/
  │ │ ├─ _about.pug
  │ │ └─ _xxxxxx.pug
  │ └─ _layout.pug
  ├─ test/
  │ ├─ test.pug
  │ └─ index.pug
  └── index.pug
  │
  ├─ style/
    ├─ atoms/
    │ ├─ sp (スマホサイズ用のスタイルなど分けてます)
    │ │ ├─ _icon_xxx.sass
    │ │ └─ _xxxxxx.sass
    │ ├─ _icon_xxx.sass
    │ └─ _xxxxxx.sass
    ├─ molecules/
    │ ├─ sp
    │ │ ├─ _list_xxx.sass
    │ │ └─ _xxxxxx.sass
    │ ├─ _list_xxx.sass
    │ └─ _xxxxxx.sass
    ├─ organisms/
    │ ├─ sp
    │ │ ├─ _about.sass
    │ │ └─ _xxxxxx.sass
    │ ├─ _about.sass
    │ └─ _xxxxxx.sass
    ├─ _common.sass (headerやfooterなど全体通して共有の要素のStyle)
    ├─ _reset.sass (リセットCSS)
    ├─ _variable.sass (Sassないで使用する変数の設定や@font-faceなどを設定)
    └─ master.sass (最終的に書き出すファイル)

かなり冗長的になってしまっているかもしれないですが、Webサイトがかなりページ数が多くなって大規模になると結構みやすい設計になっていると思っています。

最終書き出しするファイルはこんな感じで @import しかしていません。

master.sass
@import ./_variable
@import ./_reset
@import ./_common

@import ./atoms/_icon_xxx
@import ./molecules/_list_xxx
@import ./organisms/_about

// SP
@import ./atoms/sp/_icon_xxx
@import ./molecules/sp/_list_xxx
@import ./organisms/sp/_about

変数を設定する_variable.sassについては色やWebフォント、スマホ用のStyleのためにmedia screenのmax-widthの値もここで設定もしています。

_variable.sass
@font-face
  font-family: 'LINESeedJP'
  src: url('/assets/fonts/LINESeedJP_OTF_Eb.woff2') format('woff2'), url('/assets/fonts/LINESeedJP_OTF_Eb.woff') format('woff'), url('/assets/fonts/LINESeedJP_TTF_Eb.ttf') format('truetype')
$WEBFONT_LINE: 'LINESeedJP', sans-serif

@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700;900&display=swap')
$WEBFONT_NOTOSANS: 'Noto Sans JP'

$FONT_BASE: $WEBFONT_NOTOSANS, Helvetica Neue, Helvetica, Arial, Hiragino Kaku Gothic ProN, Hiragino Sans, Meiryo, sans-serif

// カラー
$COL_BASE: #ccc
$COL_W: #eee
$COL_Y: #FECE31
$COL_B: #55AAF9
$COL_R: #F588B6
$COL_G: #67E0B4
$COL_BK: #000

$DE_FONT: 16 // vw換算
$COL_FONT_BASE: #000
$COL_FONT_SUB: #fff
$COL_FONT_INPUT: #999

// VW基準横幅
$base_vw: 375
$break_point: 767
$break_point_pc: 1120

@function rem($size)
  @return calc($size / $DE_FONT) + rem

@function spvw($size)
  @return calc($size / $base_vw * 100) + vw

一例としてファイルを紹介しましたがこんな感じでPugとSassのディレクトリとファイルを設計してマークアップコーディングをしています。

Viteを使用しているので、本来はmain.tsをうまく活用すべきなのかなと思いましたが、静的環境という点と過去に染み付いた手癖に合わせる感じになりました。

ビルド時のファイル名のハッシュを辞める

便利な機能ではありますが、場合によってはハッシュがない方が差分を取れるなど利点はある。

ViteのrollupOptionsに設定があるのでこれを変更します。
https://rollupjs.org/configuration-options/#output-entryfilenames

vite.config.js
    rollupOptions: {
-       input: htmlFiles
+       input: htmlFiles,

+       output: {
+         entryFileNames: `assets/[name].js`,
+         chunkFileNames: `assets/[name].js`,
+         assetFileNames: (assetInfo) => {
+           const {name} = assetInfo
+           if (/\.(jpe?g|png|gif|svg)$/.test(name ?? '')) {
+             return 'assets/img/[name][extname]';
+           }
+           if (/\.(woff?2|ttf|otf)$/.test(name ?? '')) {
+             return 'assets/fonts/[name][extname]';
+           }
+           return 'assets/[name][extname]';
+         }
+       }
    },

画像やフォントなど区分けしておきたいファイルについてはif文で処理しています。

まとめ

Viteである必要があるのかと言われると難しいですが、個人的に動けばいいなと思っています。

静的ページを作成するのはViteでなくてもできますが、他フレームワークよりは仕様を理解していなくてもPugとSassがわかっていれば使用できるぐらいの環境を流用できるのが個人的に良い落とし所になった気がします。

先人様のナレッジを前提としての個人的備忘録として記事になりましたが、参考になったらと思います。
https://zenn.dev/yend724/articles/20220408-tfq16buha8ctdzp7

Discussion