Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 27 additions & 28 deletions config/postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
const autoprefixer = require('autoprefixer')({})
const cssnano = require('cssnano')({
preset: ['advanced', {
discardUnused: {
fontFace: false // Preserve all @font-face declarations
}
}]
})
const purgeImport = require('@fullhuman/postcss-purgecss')
const purgeCSSPlugin = purgeImport.purgeCSSPlugin || purgeImport.default || purgeImport
const purgecss = purgeCSSPlugin({
Expand All @@ -17,18 +9,25 @@ const purgecss = purgeCSSPlugin({
dynamicAttributes: ['aria-expanded', 'data-bs-theme', 'data-bs-main-theme', 'data-bs-theme-animate', 'data-transparent', 'role'],
fontFace: false,
safelist: {
// Every entry below is a class PurgeCSS cannot see in hugo_stats.json because it is
// added by JavaScript at runtime (or by a third-party library). Classes that Hugo DOES
// emit into the markup — and therefore into hugo_stats.json — are intentionally NOT
// listed: purge now always runs against complete, current stats (production
// post-process; dev does not purge), so they survive on their own. In particular the
// former `/^d-(sm|md|lg|xl|xxl)-table-cell$/` entry is gone — render-table.html emits
// the configured breakpoint's class into the markup, so it reaches the stats naturally.
standard: [
// Bootstrap form validation
// Bootstrap form validation — added by JS on submit, never in the source markup
'was-validated',
// Bootstrap dynamic states
// Bootstrap component state classes toggled by JS (modal/dropdown/collapse/etc.)
'show',
'showing',
'hiding',
'active',
'disabled',
'collapsed',
'collapsing',
// SimpleDatatables modifier classes
// SimpleDatatables modifier classes (set by the datatables JS)
'no-header',
'no-footer',
// SimpleDatatables table rendering classes (added by JS)
Expand All @@ -38,24 +37,15 @@ const purgecss = purgeCSSPlugin({
'both',
'desc',
'asc',
// Hinode wrapped tables.
//
// On a data table `table-wrap` and `table-border-bottom-wrap` are applied in the browser by
// SimpleDatatables' tableRender hook, so on a site whose only wrapped tables are data tables
// they never reach hugo_stats.json at all and would otherwise be purged.
// Hinode wrapped tables: on a DATA table, `table-wrap` / `table-border-bottom-wrap`
// are applied in the browser by SimpleDatatables' tableRender hook, so on a site whose
// only wrapped tables are data tables they never reach hugo_stats.json and would
// otherwise be purged.
'table-wrap',
'table-border-bottom-wrap',
// `d-none` is also applied by that hook, but a dozen core layouts emit it as well, so it can
// never actually be purged. It is listed to record that the wrap depends on it.
// `d-none` is also applied by that hook; kept to record the wrap's dependency on it
// (a dozen core layouts emit it too, so in practice it is always in the stats).
'd-none',
// `d-{breakpoint}-table-cell` reveals the wrapped column above the breakpoint, and
// render-table.html is its only emitter in the whole theme. Unlike the classes above it does
// reach hugo_stats.json - but PurgeCSS is fed the *previous* build's stats whenever PostCSS
// is not deferred to Hugo's post-process phase (`hugo server`, or any non-production build
// with style.purge enabled). A site whose committed stats predate its first wrapped table
// would therefore lose the rule, leaving the wrapped column's cells on `d-none` alone and so
// hidden at every width.
/^d-(sm|md|lg|xl|xxl)-table-cell$/,
// SimpleDatatables search component
'search-data-table',
'search-input',
Expand Down Expand Up @@ -131,11 +121,20 @@ const purgecss = purgeCSSPlugin({
]
}
})
const autoprefixer = require('autoprefixer')({})
const cssnano = require('cssnano')({
// Default preset: structural minification (rule/declaration merging) that Hugo's own
// minify does not do. The `advanced` preset was measured to add < 0.2% over `default`,
// so it — and the cssnano-preset-advanced dependency — was dropped.
preset: 'default'
})

module.exports = {
// Order matters: purge first (drop unused rules) so autoprefixer and cssnano only work
// on what ships; cssnano last so it minifies the already-prefixed output.
plugins: [
purgecss,
autoprefixer,
cssnano,
purgecss
cssnano
]
}
6 changes: 6 additions & 0 deletions exampleSite/config/_default/hugo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ home = ["HTML", "RSS", "REDIR", "netlify", "server"]
[security.http]
methods = ['(?i)GET|POST|HEAD']
urls = ['.*']
[security.node]
[security.node.permissions]
# exampleSite/config/postcss.config.js re-exports the theme root config one level up;
# Hugo runs PostCSS's Node process with fs reads limited to the site dir by default, so
# widen it to the parent config dir. A normal site keeps a self-contained config.
allowread = ['.', '../config']

[privacy]
[privacy.vimeo]
Expand Down
148 changes: 8 additions & 140 deletions exampleSite/config/postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,140 +1,8 @@
const autoprefixer = require('autoprefixer')({})
const cssnano = require('cssnano')({
preset: ['advanced', {
discardUnused: {
fontFace: false // Preserve all @font-face declarations
}
}]
})
const purgeImport = require('@fullhuman/postcss-purgecss')
const purgeCSSPlugin = purgeImport.purgeCSSPlugin || purgeImport.default || purgeImport
const purgecss = purgeCSSPlugin({
content: ['./hugo_stats.json'],
defaultExtractor: (content) => {
const els = JSON.parse(content).htmlElements
return [...(els.tags || []), ...(els.classes || []), ...(els.ids || [])]
},
dynamicAttributes: ['aria-expanded', 'data-bs-theme', 'data-bs-main-theme', 'data-bs-theme-animate', 'data-transparent', 'role'],
fontFace: false,
safelist: {
standard: [
// Bootstrap form validation
'was-validated',
// Bootstrap dynamic states
'show',
'showing',
'hiding',
'active',
'disabled',
'collapsed',
'collapsing',
// SimpleDatatables modifier classes
'no-header',
'no-footer',
// SimpleDatatables table rendering classes (added by JS)
'th-inner',
'sortable',
'sortable-center',
'both',
'desc',
'asc',
// Hinode wrapped tables.
//
// On a data table `table-wrap` and `table-border-bottom-wrap` are applied in the browser by
// SimpleDatatables' tableRender hook, so on a site whose only wrapped tables are data tables
// they never reach hugo_stats.json at all and would otherwise be purged.
'table-wrap',
'table-border-bottom-wrap',
// `d-none` is also applied by that hook, but a dozen core layouts emit it as well, so it can
// never actually be purged. It is listed to record that the wrap depends on it.
'd-none',
// `d-{breakpoint}-table-cell` reveals the wrapped column above the breakpoint, and
// render-table.html is its only emitter in the whole theme. Unlike the classes above it does
// reach hugo_stats.json - but PurgeCSS is fed the *previous* build's stats whenever PostCSS
// is not deferred to Hugo's post-process phase (`hugo server`, or any non-production build
// with style.purge enabled). A site whose committed stats predate its first wrapped table
// would therefore lose the rule, leaving the wrapped column's cells on `d-none` alone and so
// hidden at every width.
/^d-(sm|md|lg|xl|xxl)-table-cell$/,
// SimpleDatatables search component
'search-data-table',
'search-input',
// Bootstrap utilities used by SimpleDatatables
'float-right',
'float-left'
],
// Classes with these patterns will be preserved along with their children
deep: [
// Bootstrap components that get dynamically modified
/modal/,
/dropdown/,
/carousel/,
/tooltip/,
/popover/,
/collapse/,
/offcanvas/,
// SimpleDatatables - preserve structure and all nested elements
/datatable/,
// Bootstrap form controls (used by SimpleDatatables)
/form-select/,
/form-control/,
// Bootstrap button groups (used by SimpleDatatables search)
/btn-group/,
// Bootstrap responsive tables (used by list component)
/table-responsive/,
// Syntax highlighting - preserve Chroma classes and descendants
/chroma/,
/syntax-highlight/,
/codeblock/
],
// Preserve any selector containing these patterns
greedy: [
// Third-party library prefixes (well-namespaced, safe to use greedy)
/^fa-/, // FontAwesome
/^leaflet-/, // Leaflet maps
/^katex-/, // KaTeX math (note: using katex- not just katex)
/^mermaid/, // Mermaid diagrams
/datatable/, // SimpleDatatables (all variants: datatable-*, *-datatable, etc.)
/^cky-/, // CookieYes

// Component-specific prefixes
/clipboard-/, // Clipboard component
/command-/, // Command component
/search-/, // Search functionality (includes search-input, search-data-table)
/suggestion__/, // Search suggestions (FlexSearch)
/testimonial-/, // Testimonial component
/preview-/, // Preview component (mod-blocks)

// Syntax highlighting - third-party engines (Chroma handled in deep)
/^hljs-/, // highlight.js
/^language-/, // Prism/generic

// Pagination and navigation
/page-item/,
/page-link/,
/pagination/, // Bootstrap pagination classes
/nav-item/,
/nav-link/,
/navbar-/,
/^nav-/, // Nav variant classes (nav-callout, nav-panel, nav-pills, nav-tabs, nav-underline)

// Bootstrap responsive tables
/table-responsive/, // All table-responsive-* variants and attribute selectors

// Color mode toggle - d-none-main-* classes plus [data-bs-main-theme="dark"] compound selectors
/d-none-main/,

// Bootstrap transitions and utilities that get added via JS
/fade/,
/^translate/ // Bootstrap utilities
]
}
})

module.exports = {
plugins: [
autoprefixer,
cssnano,
purgecss
]
}
// The exampleSite reuses the theme's PostCSS pipeline verbatim to avoid config drift.
// `content: ['./hugo_stats.json']` inside it stays cwd-relative, so it resolves to the
// exampleSite's own stats file when Hugo runs PostCSS with the exampleSite as the site root.
// NOTE: Hugo runs PostCSS's Node process under its permission model
// (`security.node.permissions.allowread`), which defaults to the site dir only. Reading this
// shared config lives one level up, so the exampleSite widens allowread to '../config' in
// config/_default/hugo.toml. A normal site keeps a self-contained config and needs no change.
module.exports = require('../../config/postcss.config.js')
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@
"documentation"
],
"main": "index.js",
"browserslist": [
">= 0.5%",
"last 2 major versions",
"not dead",
"Chrome >= 60",
"Firefox >= 60",
"Firefox ESR",
"iOS >= 12",
"Safari >= 12",
"not Explorer <= 11",
"not kaios <= 2.5"
],
"private": false,
"packageManager": "pnpm@11.8.0+sha512.c1f5e7c4cb241c8f174b743851d82f42b802324afc8b0f116b96adb15aa06664948dde36960a3ba1079ba5b4b29dd0140135b94b5b5f5263592249d68e555f26",
"publishConfig": {
Expand Down Expand Up @@ -79,7 +91,6 @@
"@fullhuman/postcss-purgecss": "^8.0.0",
"autoprefixer": "^10.5.3",
"cssnano": "^8.0.2",
"cssnano-preset-advanced": "^8.0.2",
"hugo-extended": "^0.164.0",
"postcss-cli": "^11.0.1",
"rimraf": "^6.1.3"
Expand Down
Loading