Структура шаблона WP и настройка Webpack

Привет всем. Данный мануал не претендует на истину, но такая структура проекта-шаблона для WordPress мне кажется максимально понятной и удобной.

В настоящий момент именно так я собираю большинство несложных клиентских сайтов, используя WP и ACF Pro для создания Gutenberg-блоков.

1. package.json

{
	"name": "project-name",
	"version": "1.0.0",
	"description": "",
	"main": "webpack.config.js",
	"scripts": {
		"test": "echo \"Error: no test specified\" && exit 1"
	},
	"author": "",
	"license": "ISC",
	"devDependencies": {
		"autoprefixer": "^9.6.1",
		"css-loader": "^3.2.0",
		"file-loader": "^4.2.0",
		"mini-css-extract-plugin": "^0.8.0",
		"node-sass": "^4.12.0",
		"optimize-css-assets-webpack-plugin": "^5.0.3",
		"postcss-cli": "^6.1.3",
		"postcss-loader": "^3.0.0",
		"postcss-scss": "^2.0.0",
		"sass-loader": "^8.0.0",
		"uglifyjs-webpack-plugin": "^2.2.0",
		"url-loader": "^2.2.0",
		"webpack": "^4.39.2",
		"webpack-cli": "^3.3.7",
		"webpack-fix-style-only-entries": "^0.4.0"
	},
	"browserslist": [
		"last 2 version"
	]
}

2. Структура WP шаблона (темы)

assets/
    fonts/
    images/
    scripts/ // js-скрипты
        contact-form.js
        ajax-posts.js
    styles/ // стили
        fonts.scss
        reset.scss
        style.scss
        variables.scss
    vendor/ // библиотеки, хотя я обычно подключаю с cdn. В сборке не участвуют, подключаются по WP API
        juxtapose/ 
            juxtapose.css
            juxtapose.js
    blocks.js // точка входа стилей и скриптов блоков
    blocks.min.css // скомпилированный css блоков для фронта и гутенберга
    blocks.min.js // скомпилированный js блоков для фронта и гутенберга
    main.js // точка входа стилей и скриптов сайта
    main.min.css // скомпилированный css из папки styles
    main.min.js // скомпилированный js из папки scripts
inc/ // дополнительный функционал шаблона, например, CPT, хуки, фильтры и тп.
    shortcodes/ // папка с шорткодами
        works.php // например, файл, где создаем шорткод [works]
    acf-blocks.php // регистрация acf-блоков
    acf-fields.php // регистрация полей для блоков
    ajax-posts.php // колбек функция подгрузки постов
    post-types.php // регистрация кастомных типов записей
languages/
template-parts/
    blocks/ // папка с темплейтами acf-блоков
        slider/ // слайдер
            slider.js
            slider.scss
            slider.php
        gallery/ // галерея
            gallery.js
            gallery.scss
            gallery.php
        about/ // блок "о нас"
            about.scss
            about.php
    content-page.php // для page.php
    content-single.php // для single.php
    loop-category.php // обычно ипользую в index.php
404.php
footer.php
functions.php
header.php
index.php
package.json // в корне шаблона
page.php
postcss.config.js // здесь пропишем автопрефиксер
single.php
style.css
webpack.config.js // конфигурация webpack
screenshot.png

3. webpack.config.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const FixStyleOnlyEntriesPlugin = require("webpack-fix-style-only-entries");
const path = require("path");

module.exports = {
	watch: true,
	mode: 'development',
	entry: {
		'main': './assets/main.js', // точка входа стилей и скриптов сайта
		'blocks': './assets/blocks.js', // точка входа стилей и скриптов acf-блоков
	},
	output: {
		path: path.resolve(__dirname, 'assets'),
		filename: "[name].min.js"
	},
	module: {
		rules: [{
			test: /\.scss/,
			exclude: /node_modules/,
			use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader', { loader: 'postcss-loader', options: { config: { path: 'postcss.config.js' } } }]
		    },
		    {
			test: /\.svg/, 
			loader: 'url-loader?limit=100000'
		    },
		    {
                    test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
                    use: [{
        	        loader: 'file-loader',
                        options: {
            	            name: 'fonts/[name].[ext]',
            	            loader: 'url-loader?limit=10000&mimetype=application/font-woff',            	
                        }
                    }]
        	}],
	},
	plugins: [new FixStyleOnlyEntriesPlugin({ silent: true }), new MiniCssExtractPlugin({ filename: '[name].min.css' }), ]
};

4. postcss.config.js

const autoprefixer = require('autoprefixer');

module.exports = {
	plugins: [
	    autoprefixer(), 
        ],
};

Для запуска webpack я использую команду:

npx webpack

5. main.js

// исходные стили
import './styles/reset.scss';
import './styles/fonts.scss';
import './styles/style.scss';

// исходные js-файлы
import './scripts/contact-form';
import './scripts/ajax-posts';

На выходе получаем скомпилированный main.min.css и main.min.js

6. blocks.js

// блок Слайдер
import '../template-parts/blocks/slider/slider.scss';
import '../template-parts/blocks/slider/slider'; // инициализация слайдера

// блок Галерея
import '../template-parts/blocks/gallery/gallery.scss';
import '../template-parts/blocks/gallery/gallery'; // инициализация галереи

// блок О нас
import '../template-parts/blocks/about/about.scss';

На выходе получаем скомпилированный blocks.min.css и blocks.min.js


Почему я не использую зависимости типа jquery и других фреймворков при сборке бандла?
Все просто — некоторые блоки могут редко встречаться и использовать увесистые библиотеки, а jquery часто самостоятельно подключают сторонние плагины. Таким образом с целью уменьшения итогового размера бандлов я оставил классическое подключение jquery для всего сайта в function.php. В итоге подключение собранных стилей и скриптов выглядит так:

// Фронт-енд
function ay_scripts() {	
	$theme_version = wp_get_theme()->get( 'Version' );		
	wp_enqueue_style( 'blocks', get_template_directory_uri() . '/assets/blocks.min.css', null, $theme_version );	
	wp_enqueue_style( 'main', get_template_directory_uri() . '/assets/main.min.css', null, $theme_version );
	wp_deregister_script( 'jquery' );	
	wp_register_script( 'jquery', 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js', array(), '3.4.1', true );	
	wp_enqueue_script( 'main', get_template_directory_uri() . '/assets/main.min.js', array( 'jquery' ), $theme_version, true );
	wp_enqueue_script( 'blocks', get_template_directory_uri() . '/assets/blocks.min.js', array( 'jquery' ), $theme_version, true );
}
add_action( 'wp_enqueue_scripts', 'ay_scripts' );

// Внешний вид и функционал блоков в админке в Gutenberg
function admin_css() {			
	wp_enqueue_style( 'blocks', get_template_directory_uri() . '/assets/blocks.min.css', null, null );
	wp_enqueue_script( 'blocks', get_template_directory_uri() . '/assets/blocks.min.js', array(), null, true );
}
add_action( 'enqueue_block_editor_assets', 'admin_css' );

А вот так в ACF-блоке на примере кастомного блока «Галерея»:

acf_register_block( array (
	'name' => 'gallery',
	'title' => __( 'Галерея', 'ay' ),
	'description' => '',
	'mode' => 'edit',
	'category' => 'common',
	'icon' => 'images-alt',
	'keywords' => array( 'images' ),
	'supports' => array( 'align' => false ),
	'render_template' => get_template_directory() . '/template-parts/blocks/gallery/gallery.php',
	'enqueue_assets' => function() {
		wp_enqueue_style( 'fancybox', 'https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css', '3.5.7', null );
		wp_enqueue_script( 'fancybox', 'https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.js', array(), '3.5.7', true );
	}
) );