Простой статический сайт на Webpack 5

Давно уже не верстал статичные сайты, но тут появилась халтурка от хороших людей. Отказать не смог, вручную верстать уже не тот вайб, а старые сборки которые у меня были, они уже совсем устарели, хотя и на них можно сделать, как в старые добрые времена. Решил задаться вопросом, попробовать чтото более новое, может чтото уже появилось? Пошло поехало, начал собирать простой статический сайт из нескольких HTML-страниц. Хотелось использовать современные инструменты, но без лишних сложностей вроде React или Vue, т.к. они не подходили под задачу, верстку под битрикс. В итоге остановился на Webpack 5 — он отлично справляется с такой задачей.

В этой статье хотел бы рассказать, как сделать сборку статического сайта с помощью Webpack 5. Проект собирает статичные HTML страницы, компилирует SASS в CSS, объединяет JavaScript-файлы и автоматически создаёт SVG-спрайты для иконок.

Что мы получим в итоге

В результате получится проект, который:

  • собирает несколько HTML страниц из шаблонов с общими header и footer

  • компилирует SASS/SCSS в один CSS-файл

  • объединяет JavaScript-код и библиотеки в один bundle

  • создаёт inline SVG-спрайт для иконок

  • минифицирует код для production

Сборка работает на последней версии webpack 5, который на данный момент является стандартом для сборки фронтенд-проектов под статичную верстку.

Структура проекта

Структуру папок выглядит так:

.
├── dist                 - папка с собранным сайтом
├── src                  - исходники
│   ├── favicon         - иконки сайта
│   ├── fonts           - шрифты
│   ├── html            - HTML-шаблоны
│   │   ├── includes    - общие части (header, footer)
│   │   └── views       - страницы сайта
│   ├── icons           - SVG-иконки для спрайта
│   ├── img             - изображения
│   ├── js              - JavaScript-файлы
│   ├── scss            - стили SASS/SCSS
│   └── uploads         - дополнительные файлы
├── package.json
└── webpack.config.js

Папка icons нужна специально для SVG-иконок, которые будут автоматически собираться в спрайт. Обычные изображения идут в img.

1 шаг. Начальная настройка

Создаём новый проект и инициализируем его:

npm init

Терминал может предлагать варианты настройки, я выбрал все по умолчанию. После этого устанавливаем базовые пакеты Webpack:

npm install webpack webpack-cli webpack-dev-server --save-dev

Теперь у нас в package.json появились зависимости. Теперь у нас появились нужные зависимости, приступим к настройке сборки.

Сборка JavaScript

Начнём с JavaScript, так как это основа Webpack. В проекте я буду использовать Bootstrap 5, jQuery и Popper.js, т.к. это является основополагающим для верстки(можно обойтись только Jquery) поэтому установим их:

npm install jquery  --save
npm install bootstrap @popperjs/core --save

Обращу внимание, что эти пакеты нужны для самого сайта, поэтому ставим их с флагом --save, а не --save-dev(только для разработки).

Теперь создад��м файл webpack.config.js с базовой конфигурацией:

const path = require("path");

module.exports = {
  entry: ["./src/js/index.js", "./src/scss/style.scss"],
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "js/bundle.js",
    clean: true,
  },
  devtool: "source-map",
};

В entry мы указываем пути по которым подключаются - главный JS-файл и главный SCSS-файл. Указываем параметр clean: true чтобы автоматически очистить папку dist перед каждой сборкой.

В src/js/index.js подключаем библиотеки или статичные файлы JS:

import "bootstrap";
import "./static-js";

Здесь подключаем Bootstrap и наш собственный JavaScript-код. Если нужен jQuery глобально, можно добавить:

import $ from "jquery";
window.$ = window.jQuery = $;

Сборка стилей CSS из SASS

Для работы с SASS нужны несколько пакетов:

npm install sass sass-loader css-loader mini-css-extract-plugin --save-dev

mini-css-extract-plugin — это современная замена старому extract-text-webpack-plugin. Он извлекает CSS в отдельный файл, что удобно для многостраничных сайтов.

Добавим в конфиг вебпака webpack.config.js:

const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  entry: ["./src/js/index.js", "./src/scss/style.scss"],
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "js/bundle.js",
    clean: true,
  },
  devtool: "source-map",
  module: {
    rules: [
      {
        test: /\.(scss|sass)$/,
        include: path.resolve(__dirname, "src/scss"),
        use: [
          MiniCssExtractPlugin.loader,
          { loader: "css-loader", options: { sourceMap: true, url: false } },
          { loader: "sass-loader", options: { sourceMap: true } },
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: "css/style.bundle.css" }),
  ],
};

Устанавливаем параметр url: false в css-loader чтобы не обрабатывать пути к файлам в CSS (шрифты, изображения). Эт�� удобно, когда файлы копируются отдельно через CopyPlugin.

Файл src/scss/style.scss является главным, где подключаются все стили разбитые на группы, чтобы проще было ориентироваться:

// ===================  Modules  ========================

@import 'modules/reset';
@import 'utilities/variables';
@import 'utilities/mixins';
@import 'utilities/utils';
@import 'modules/mixin_font-face';
@import 'modules/fontstylesheet';

// ====================  Plugins  =======================
// Bootstrap

@import '../../node_modules/bootstrap/scss/mixins/breakpoints';
@import '../../node_modules/bootstrap/scss/bootstrap-grid';

// ====================  Default  =======================

@import "elements/ui";
@import "elements/typography";
@import "elements/buttons";
@import "elements/inputs";
@import "elements/forms";
@import "elements/icons";
@import "layout/general";

// ====================  components  ======================

@import 'components/header';
@import 'components/footer';
@import 'components/modals';
@import 'components/main-info-boxes';

// ====================  Pages  ======================
@import "pages/pages";

// =====================  Media  ========================

@import "modules/print";

SVG-спрайты для иконок

Одна из крутых фишек этого проекта — автоматическое создание SVG-спрайта. Вместо того чтобы вручную собирать иконки в один файл, просто кладём SVG-файлы в папку src/icons, и они автоматически попадут в inline-спрайт, удобно и практично изменять цвета иконок при каких то событиях

Установим нужный лоадер:

npm install svg-sprite-loader --save-dev

Добавляем правило в webpack.config.js для svg sprite:

{
  test: /\.svg$/,
  include: path.resolve(__dirname, "src/icons"),
  use: [
    {
      loader: "svg-sprite-loader",
      options: {
        symbolId: "icon-[name]",
      },
    },
  ],
},

префикс "icon-" можно убрать чтобы обращаться сразу по имени файла svg.

Важно: это правило применяется только к файлам из папки src/icons. Остальные SVG обрабатываются как обычные изображения.

В src/js/index.js добавляем автоматический импорт всех иконок:

const importAll = (r) => r.keys().forEach(r);
importAll(require.context("../icons", false, /\.svg$/));

Теперь все SVG из папки icons автоматически попадут в спрайт. Использовать их можно так:

<svg class="icon">
  <use xlink:href="#icon-logo"></use>
</svg>

Имена иконок формируются как icon-<имя-файла>. Например, файл logo.svg станет #icon-logo.

Сборка HTML-страниц

Для сборки HTML используем html-webpack-plugin. Он умеет работать с шаблонами и автоматически подставлять пути к CSS и JS.

npm install html-webpack-plugin raw-loader --save-dev

raw-loader нужен для интеграции частей шаблонов например header или footer.

В проекте используется lodash-шаблонизатор.
Вот пример страницы src/html/views/index.html:

<% var data = {
  title: "Главная страница",
  copyright: "2025"
}; %>
<%= _.template(require('./../includes/header.html').default)(data) %>

<div class="container">
  <h1>Контент страницы</h1>
</div>

<%= _.template(require('./../includes/footer.html').default)(data) %>

В includes/header.html содержится html верстка шапки сайта и если нужно внести какието изменения в шапку сайта, то мы вносим их в одном только файле и она меняется на всем сайте.

В header.html можно использовать переменные из data:

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <title><%=title%></title>
</head>
<body>

Чтобы автоматически генерировать HTML для всех страниц из папки views, добавляем функцию в webpack.config.js:

const fs = require("fs");
const HtmlWebpackPlugin = require("html-webpack-plugin");

function generateHtmlPlugins(templateDir) {
  const templateFiles = fs.readdirSync(path.resolve(__dirname, templateDir));
  return templateFiles.map((item) => {
    const parsedPath = path.parse(item);
    const name = parsedPath.name;
    const extension = parsedPath.ext.substring(1);
    return new HtmlWebpackPlugin({
      filename: `${name}.html`,
      template: path.resolve(__dirname, `${templateDir}/${name}.${extension}`),
      inject: true,
      scriptLoading: "blocking",
    });
  });
}

const htmlPlugins = generateHtmlPlugins("src/html/views");

И добавляем правило для обработки includes:

{
  test: /\.html$/,
  include: path.resolve(__dirname, "src/html/includes"),
  use: ["raw-loader"],
},

В plugins добавляем:

plugins: [
  // ... другие плагины
].concat(htmlPlugins),

Теперь каждая страница из src/html/views автоматически соберётся в отдельный HTML-файл.

Копирование статических файлов

При сборке production версии, мы должны скопировать изображений, шрифты и другие файлы, для этого используем copy-webpack-plugin:

npm install copy-webpack-plugin --save-dev

В конфиг webpack добавляем:

const CopyPlugin = require("copy-webpack-plugin");

// В plugins:
new CopyPlugin({
  patterns: [
    { from: "src/fonts", to: "fonts", noErrorOnMissing: true },
    { from: "src/favicon", to: "favicon", noErrorOnMissing: true },
    { from: "src/img", to: "img", noErrorOnMissing: true },
    { from: "src/uploads", to: "uploads", noErrorOnMissing: true },
  ],
}),

noErrorOnMissing: true означает, что если папка отсутствует, ошибки не появится.

Обработка изображений

Для обработки изображений (кроме SVG из папки icons) добавим правило:

{
  test: /\.(png|jpe?g|gif|svg|webp|avif)$/i,
  exclude: path.resolve(__dirname, "src/icons"),
  type: "asset",
  parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
  generator: { filename: "img/[name][ext]" },
},

Файлы меньше 8 КБ будут встроены в код как base64, остальные скопируются в dist/img.

Шрифты:

{
  test: /\.(woff|woff2|eot|ttf|otf)$/i,
  type: "asset/resource",
  generator: { filename: "fonts/[name][ext]" },
},

Финальная стадия, оптимизация для production

Для production-сборки добавим минификацию CSS и JS. Установим плагины:

npm install css-minimizer-webpack-plugin terser-webpack-plugin --save-dev

В конфиге нужно добавить для оптимизации production сборки:

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");

module.exports = (env, argv) => {
  const mode = argv.mode || "development";
  const config = {
    // ... базовая конфигурация
  };

  if (mode === "production") {
    config.optimization = {
      minimize: true,
      minimizer: [
        new CssMinimizerPlugin({
          minimizerOptions: {
            preset: ["default", { discardComments: { removeAll: true } }],
          },
        }),
        new TerserPlugin({
          extractComments: true,
          terserOptions: { compress: { drop_console: true } },
        }),
      ],
      splitChunks: {
        chunks: "all",
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: "vendors",
            chunks: "all",
          },
        },
      },
    };
  }

  return config;
};

В production-режиме код сжимается, минифицируется, удаляются комментарии и убираются console.log. Разделяем код на chunks для лучшего кеширования.

Настройка dev-сервера для разработки

Для удобной разработки настраиваем dev-сервер:

devServer: {
  static: { directory: path.join(__dirname, "dist") },
  port: 9000,
  hot: true,
  open: true,
  watchFiles: ["src/**/*"],
},

Сервер запускается на порту 9000(можете указать свой порт), автоматически открывает браузер и следит за изменениями файлов.

Донастраиваем package.json

Добавляем удобные команды:

{
  "scripts": {
    "dev": "webpack --mode development",
    "watch": "webpack --mode development --watch",
    "start": "webpack serve --no-client-overlay-warnings --open",
    "build": "webpack --mode production && prettier --print-width=120 --parser html --write dist/*.html"
  }
}
  • npm run dev — однократная сборка в режиме разработки

  • npm run watch — сборка с отслеживанием изменений

  • npm start — запуск dev-сервера с автоматическим открытием сайта в браузере

  • npm run build — production-сборка с форматированием HTML через Prettier

Итоговая структура webpack.config.js

Вот так выглядит полный конфиг:

const path = require("path");
const fs = require("fs");
const CopyPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");

function generateHtmlPlugins(templateDir) {
  const templateFiles = fs.readdirSync(path.resolve(__dirname, templateDir));
  return templateFiles.map((item) => {
    const parsedPath = path.parse(item);
    const name = parsedPath.name;
    const extension = parsedPath.ext.substring(1);
    return new HtmlWebpackPlugin({
      filename: `${name}.html`,
      template: path.resolve(__dirname, `${templateDir}/${name}.${extension}`),
      inject: true,
      scriptLoading: "blocking",
    });
  });
}

const htmlPlugins = generateHtmlPlugins("src/html/views");

const baseConfig = {
  entry: ["./src/js/index.js", "./src/scss/style.scss"],
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "js/bundle.js",
    clean: true,
    assetModuleFilename: "assets/[name][ext]",
  },
  devtool: "source-map",
  devServer: {
    static: { directory: path.join(__dirname, "dist") },
    port: 9000,
    hot: true,
    open: true,
    watchFiles: ["src/**/*"],
  },
  module: {
    rules: [
      {
        test: /\.(scss|sass)$/,
        include: path.resolve(__dirname, "src/scss"),
        use: [
          MiniCssExtractPlugin.loader,
          { loader: "css-loader", options: { sourceMap: true, url: false } },
          { loader: "sass-loader", options: { sourceMap: true } },
        ],
      },
      {
        test: /\.html$/,
        include: path.resolve(__dirname, "src/html/includes"),
        use: ["raw-loader"],
      },
      {
        test: /\.(png|jpe?g|gif|svg|webp|avif)$/i,
        exclude: path.resolve(__dirname, "src/icons"),
        type: "asset",
        parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
        generator: { filename: "img/[name][ext]" },
      },
      {
        test: /\.svg$/,
        include: path.resolve(__dirname, "src/icons"),
        use: [
          {
            loader: "svg-sprite-loader",
            options: {
              symbolId: "icon-[name]",
            },
          },
        ],
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: "asset/resource",
        generator: { filename: "fonts/[name][ext]" },
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: "css/style.bundle.css" }),
    new CopyPlugin({
      patterns: [
        { from: "src/fonts", to: "fonts", noErrorOnMissing: true },
        { from: "src/favicon", to: "favicon", noErrorOnMissing: true },
        { from: "src/img", to: "img", noErrorOnMissing: true },
        { from: "src/uploads", to: "uploads", noErrorOnMissing: true },
      ],
    }),
  ].concat(htmlPlugins),
};

module.exports = (env, argv) => {
  const mode = argv.mode || "development";
  baseConfig.mode = mode;

  if (mode === "production") {
    baseConfig.devtool = "source-map";
    baseConfig.output.filename = "js/[name].js";
    baseConfig.optimization = {
      minimize: true,
      minimizer: [
        new CssMinimizerPlugin({
          minimizerOptions: {
            preset: ["default", { discardComments: { removeAll: true } }],
          },
        }),
        new TerserPlugin({
          extractComments: true,
          terserOptions: { compress: { drop_console: true } },
        }),
      ],
      splitChunks: {
        chunks: "all",
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: "vendors",
            chunks: "all",
          },
        },
      },
      runtimeChunk: "single",
      moduleIds: "named",
      chunkIds: "named",
    };
  } else {
    baseConfig.devtool = "eval-source-map";
    baseConfig.output.filename = "js/bundle.js";
    baseConfig.optimization = {
      minimize: false,
      splitChunks: false,
      runtimeChunk: false,
    };
  }

  return baseConfig;
};
```</spoiler>

## Что в итоге получилось

В итоге мы получили удобный и быстрый инструмент для сборки статических сайтов. 
Хотел бы отметить его плюсы для скорости разработки:
- собирает HTML-страницы из шаблонов и можно подключить отдельно модули, детали сайта(header, footer, модальные окна) которые будут использованы на всех остальных страницах.
- компилирует и оптимизирует SASS в CSS
- объединять минифицирует JavaScript
- автоматически создает SVG-спрайт
- копирует статические файлы для финальной сборки
- минификация и очистка от ненужных комментариев и console.log код для production

Всё это работает на Webpack 5 с современными подходами. Данную сборку можно взять как за основу для верстки.

## Полезные мелочи

**SASS-миксины для медиазапросов.** В проекте есть удобные миксины для работы с брейкпоинтами, медиа эндпоинты можно найти в scss/utilities/_variables.sass:

$xss: 360
$xs: 450
$sm: 600
$md: 768
$lg: 1023
$xxl: 1160
$xl: 1200
$hd: 1440


```sass
=r($width)
  @media only screen and (max-width: $width + "px")
    @content

=rmin($width)
  @media only screen and (min-width: $width + "px")
    @content

Использование:

.block
  font-size: 14px
 +r($md)
    font-size: 16px
 +rmin(768)
    font-size: 16px

Модульная структура SASS. Стили разбиты на логические части: utilities (переменные, миксины), elements (кнопки, формы), components (header, footer), pages (стили страниц). Это удобно для больших проектов.

Готовый шаблон стартового проекта для верстки можно найти в репозитории. Там же есть примеры использования и дополнительная документация.

Комментарии (0)