switched to using webpack & svgo to optimize SVG files, and inline them via PHP

This commit is contained in:
Adam Piontek 2021-07-26 11:03:32 -04:00
parent 65810de967
commit 028efdcc67
13 changed files with 174 additions and 3693 deletions

View file

@ -19,9 +19,13 @@ The WordPress theme for 73k.us, based on Bootstrap 5 and PurgeCSS.
## Webpack
The theme uses Webpack as its bundler with ES6 modules for JavaScript files.
### SpriteLoaderPlugin
### Inline SVGs
SVG icons can be pulled into a sprite file (output to `dist/images/icon-sprites.svg`). For this to work, `@import` them in main.js (see examples). Sprite names are set by the config in `webpack.config.js` with prefixes supported for some icon packs ([@mdi/svg](https://www.npmjs.com/package/@mdi/svg), [bootstrap-icons](https://www.npmjs.com/package/bootstrap-icons), [heroicons](https://www.npmjs.com/package/heroicons)). They can then be used for menus (put `icon-<PREFIX>-<ICON-NAME>` in the class for a menu item), or used in the theme php files with the `svg_icon_use($icon_name, $div_class)` function from `custom-functions.php` to get a div containing the correct svg use tag. `$div_class` should usually include `baseline` for proper layout.
SVG images and icons can be optimized and injected inline; in order to do this, `@import` them in `main.js` (see that file for examples). Optimized output files are named per config in `webpack.config.js` with prefixes supported for some icon packs ([@mdi/svg](https://www.npmjs.com/package/@mdi/svg), [bootstrap-icons](https://www.npmjs.com/package/bootstrap-icons), [heroicons](https://www.npmjs.com/package/heroicons)), or a default prefix of `svg-`.
Insert one in the theme using `inline_svg()` function defined in `custom-functions.php` -- it takes two arguments: the icon file name (minus `.svg` extension), and a key-value array to handle svg class, outer div with class for icons, and some accessibility options. For standard square icons, use a div class if `icon baseline` - in rare situations just `icon`. For non-icon images, skip the div_class and use an svg_class as needed.
This theme also supports icons in the navbar menu items via setting `icon-<PREFIX>-<ICON-NAME>` in the class field for a menu item in the Wordpress menu editor.
## Syntax Highlighting

View file

@ -25,15 +25,15 @@ namespace WP_73k;
<div class="post-date font-monospace text-gray-300 <?php echo (has_tag() ? '' : 'mb-3'); ?>">
<?php
echo svg_icon_use("mdi-calendar-clock", "baseline me-2") . get_the_date('F j, Y');
echo ' by ' . svg_icon_use("mdi-account", "baseline me-1") . get_the_author();
echo inline_svg( 'mdi-calendar-clock', array( 'div_class' => 'icon baseline me-2' ) ) . get_the_date('F j, Y');
echo ' by ' . inline_svg( 'mdi-account', array( 'div_class' => 'icon baseline me-1' ) ) . get_the_author();
?>
</div>
<?php
if (has_tag()) {
echo '<div class="post-tags fs-smaller mb-4">';
echo svg_icon_use("mdi-tag-multiple", "baseline text-gray-300 me-1");
echo inline_svg( 'mdi-tag-multiple', array( 'div_class' => 'icon baseline text-gray-300 me-1' ) );
$tag_strings = array_map(function ($tag) {
return '<span class="text-gray-300">#</span><a href="' . get_tag_link($tag) . '">' . $tag->name . '</a>';

View file

@ -1,13 +1,55 @@
<?php
/**
* Function to support inline SVG icons by name with div wrapper
* example inline SVG function atts array supported keys
*/
function svg_icon_use($icon_name, $div_class = '') {
$div_class .= ' icon';
$output = "<div class=\"$div_class $icon_name\"><svg class=\"$icon_name\" aria-hidden=\"true\">";
$output .= "<use xlink:href=\"" . get_stylesheet_directory_uri() . "/dist/images/icon-sprites.svg#$icon_name\"></use>";
return $output . "</svg></div>";
// array(
// 'div_class' => 'icon baseline', // or 'img logo' or something
// 'svg_class' => '',
// 'svg_title' => '',
// 'role_img' => false,
// 'aria_hidden' => true
// )
/**
* inline SVG function
* desired SVG must exist in ./dist/images,
* preferably by import in main.js and processing by webpack
*/
function inline_svg( $svg_name, $atts = array() ) {
// load atts or set defaults
extract(shortcode_atts(array(
'div_class' => '',
'svg_class' => '',
'svg_title' => '',
'svg_role_img' => false,
'svg_aria_hidden' => true,
), $atts));
// load initial svg content
$svg_content = file_get_contents( get_template_directory_uri() . '/dist/images/' . $svg_name . '.svg' );
// set svg class
$class_target = $svg_class == '' ? 'class="{{class-placeholder}}"' : '{{class-placeholder}}';
// replace svg class
$svg_content = str_replace($class_target, $svg_class, $svg_content);
// handle if role=img
$svg_content = $svg_role_img ? str_replace('<svg ', '<svg role="img" ', $svg_content) : $svg_content;
// handle if aria_hidden
$svg_content = $svg_aria_hidden ? str_replace('<svg ', '<svg aria-hidden="true" ', $svg_content) : $svg_content;
// handle svg title
$svg_title = $svg_title == '' ? '' : '<title>' . $svg_title . '</title>';
$svg_content = substr_replace($svg_content, $svg_title, strpos($svg_content,'>') + 1, 0);
// handle if div class
$svg_content = $div_class == '' ? $svg_content : '<div class="' . $div_class . '">' . $svg_content . '</div>';
// return assembled svg (or div>svg)
return $svg_content;
};
?>

View file

@ -27,13 +27,13 @@ function single_social_icon_function( $atts = array() ) {
// set up default parameter
extract(shortcode_atts(array(
'name' => '0',
'class' => 'baseline'
'class' => 'icon baseline'
), $atts));
if ($name == '0') {
return 'social_icon shortcode requires "name" parameter, like "name=mdi-account"';
} else {
return svg_icon_use($name, $class);
return inline_svg( $name, array( 'div_class' => $class ) );
}
}
add_shortcode('social_icon', 'single_social_icon_function');

View file

@ -67,7 +67,7 @@ namespace WP_73k;
esc_url( home_url( '/' ) )
);
echo svg_icon_use("mdi-desktop-classic", "baseline");
echo inline_svg( 'mdi-desktop-classic', array( 'div_class' => 'icon baseline' ) );
printf( '<span class="fw-light font-brand">\\\\%1$s</span>',
esc_html( get_bloginfo( 'name' ) )

3677
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -41,7 +41,7 @@
"purgecss-webpack-plugin": "^4.x",
"sass": "^1.x",
"sass-loader": "^12.x",
"svg-sprite-loader": "^6.x",
"svgo-loader": "^3.0.0",
"webpack": "^5.x",
"webpack-cli": "^4.x"
},

View file

@ -22,7 +22,7 @@ get_header(); ?>
<header>
<h2>
<?php
echo svg_icon_use('mdi-account', 'baseline');
echo inline_svg( 'mdi-account', array( 'div_class' => 'icon baseline' ) );
echo ' ' . get_the_author_meta('display_name');
?>
</h2>

View file

@ -21,7 +21,7 @@ $seventythreek_aria_label = ! empty( $args['aria_label'] ) ? 'aria-label="' . es
<label id="<?php echo esc_attr( $seventythreek_unique_id ) . '-label'; ?>" for="<?php echo esc_attr( $seventythreek_unique_id ); ?>" aria-hidden class="form-label d-none"><?php _e( 'Search&hellip;', 'seventythreek' ); // phpcs:ignore: WordPress.Security.EscapeOutput.UnsafePrintingFunction -- core trusts translations ?></label>
<input type="search" id="<?php echo esc_attr( $seventythreek_unique_id ); ?>" class="form-control me-2 tek-search-input" value="<?php echo get_search_query(); ?>" name="s" aria-labelledby="<?php echo esc_attr( $seventythreek_unique_id ) . '-label'; ?>" placeholder="Search blog&hellip;" />
<button type="submit" class="btn btn-outline-light" title="Search">
<?php echo svg_icon_use('mdi-magnify', 'baseline'); ?>
<?php echo inline_svg( 'mdi-magnify', array( 'div_class' => 'icon baseline' ) ); ?>
</button>
</div>
</form>

View file

@ -19,7 +19,7 @@ $social_icons = array(
'target' => "_blank"
),
array('icon' => "mdi-github", 'url' => "https://github.com/apiontek", 'prof' => true, 'target' => "_blank"),
array('icon' => "gitea", 'url' => "https://73k.us/git/adam", 'prof' => true, 'target' => "_blank"),
array('icon' => "svg-gitea", 'url' => "https://73k.us/git/adam", 'prof' => true, 'target' => "_blank"),
array(
'icon' => "mdi-key-variant",
'url' => '/DF185CEE29A3D443_public_key.asc',
@ -79,7 +79,7 @@ function social_icons_str($icons_arr) {
$pad = $i == 0 ? 'pe-1' : ($i == (count($icons_arr) - 1) ? 'ps-1' : 'px-1');
$out_str .= '<a href="' . $social['url'] . '" rel="noreferrer" target="' . $social['target'];
$out_str .= '" class="fs-3 link-light text-decoration-none ' . $pad . '">';
$out_str .= svg_icon_use($social['icon'], "baseline") . "</a>";
$out_str .= inline_svg($social['icon'], array( 'div_class' => 'icon baseline' ) ) . "</a>";
}
return $out_str . '</div>';
}

View file

@ -59,7 +59,7 @@ add_filter( 'wp_nav_menu_objects', function($items, $args) {
foreach ($object->classes as $c) {
if (substr( $c, 0, strlen( $svgicon_prefix ) ) === $svgicon_prefix) {
$icon_slug = str_replace($svgicon_prefix, '', $c);
$object->title = svg_icon_use($icon_slug, 'baseline') . "\\" . $object->title;
$object->title = inline_svg( $icon_slug, array( 'div_class' => 'icon baseline' ) ) . "\\" . $object->title;
}
}
}

67
svgo.config.js Normal file
View file

@ -0,0 +1,67 @@
module.exports = {
multipass: false, // boolean. false by default
plugins: [
'cleanupAttrs',
'mergeStyles',
'inlineStyles',
'removeDoctype',
'removeXMLProcInst',
'removeComments',
'removeMetadata',
'removeTitle',
'removeDesc',
'removeUselessDefs',
'removeXMLNS',
'removeEditorsNSData',
'removeEmptyAttrs',
'removeHiddenElems',
'removeEmptyText',
'removeEmptyContainers',
// 'removeViewBox',
'cleanupEnableBackground',
'minifyStyles',
// 'convertStyleToAttrs',
'convertColors',
'convertPathData',
'convertTransform',
'removeUnknownsAndDefaults',
'removeNonInheritableGroupAttrs',
'removeUselessStrokeAndFill',
'removeUnusedNS',
// 'prefixIds',
'cleanupIDs',
'cleanupNumericValues',
// 'cleanupListOfValues',
'moveElemsAttrsToGroup',
'moveGroupAttrsToElems',
'collapseGroups',
// 'removeRasterImages',
'mergePaths',
'convertShapeToPath',
'convertEllipseToCircle',
'sortAttrs',
'sortDefsChildren',
'removeDimensions',
// 'removeAttrs',
{
name: 'removeAttrs',
params: {
attrs: 'svg:id'
}
},
// 'removeAttributesBySelector',
// 'removeElementsByAttr',
// 'addClassesToSVGElement',
{
name: 'addClassesToSVGElement',
params: {
className: '{{class-placeholder}}'
}
},
// 'addAttributesToSVGElement',
// 'removeOffCanvasPaths',
'removeStyleElement',
// 'removeScriptElement',
// 'reusePaths',
]
}

View file

@ -3,7 +3,6 @@ const glob = require("glob-all");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin');
const SpriteLoaderPlugin = require("svg-sprite-loader/plugin");
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
const PurgecssPlugin = require("purgecss-webpack-plugin");
@ -50,27 +49,32 @@ const config = {
},
{
test: /\.svg$/,
loader: "svg-sprite-loader",
options: {
extract: true,
spriteFilename: "icon-sprites.svg",
publicPath: "./images/",
symbolId: (filePath) => {
if (filePath.includes("bootstrap-icons")) {
return `bi-${path.basename(filePath).slice(0, -4)}`;
} else if (filePath.includes("@mdi")) {
return `mdi-${path.basename(filePath).slice(0, -4)}`;
} else if (filePath.includes("heroicons")) {
if (filePath.includes("outline")) {
return `hio-${path.basename(filePath).slice(0, -4)}`;
type: 'asset/resource',
generator: {
filename: (pathData) => {
if (pathData.filename.includes('@mdi')) {
return 'images/mdi-[name][ext]';
} else if (pathData.filename.includes("bootstrap-icons")) {
return 'images/bsi-[name][ext]';
} else if (pathData.filename.includes("heroicons")) {
if (pathData.filename.includes("outline")) {
return 'images/hio-[name][ext]';
} else {
return `his-${path.basename(filePath).slice(0, -4)}`;
return 'images/his-[name][ext]';
}
} else {
return `${path.basename(filePath).slice(0, -4)}`;
return 'images/svg-[name][ext]';
}
},
},
use: [
{
loader: 'svgo-loader',
options: {
configFile: path.resolve('svgo.config.js'),
}
}
],
},
]
},
@ -86,7 +90,6 @@ const config = {
},
plugins: [
new MiniCssExtractPlugin({ filename: `[name]${prefix}.css` }),
new SpriteLoaderPlugin({ plainSprite: true }),
new CopyWebpackPlugin({
patterns: [{
from: './assets/images/',