fix(css): remove position:relative from .navbar in bootstrap-subset

Post-process PurgeCSS output to remove position:relative from .navbar,
allowing CriticalCSSService's position:sticky to take effect.

This prevents bootstrap-subset.min.css from overriding the navbar
sticky positioning set by the critical CSS system.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-12-02 11:20:37 -06:00
parent 29a69617e4
commit c732b5af05
2 changed files with 328 additions and 4 deletions

324
build-bootstrap-subset.js Normal file
View File

@@ -0,0 +1,324 @@
/**
* Build Bootstrap Subset Script
*
* Genera un subset de Bootstrap con SOLO las clases usadas en el tema.
*
* USO:
* node build-bootstrap-subset.js
*
* OUTPUT:
* Assets/Vendor/Bootstrap/Css/bootstrap-subset.min.css
*/
const { PurgeCSS } = require('purgecss');
const { globSync } = require('glob');
const fs = require('fs');
const path = require('path');
async function buildBootstrapSubset() {
console.log('='.repeat(60));
console.log('Building Bootstrap Subset for ROI Theme');
console.log('='.repeat(60));
const themeDir = __dirname;
const inputFile = path.join(themeDir, 'Assets/Vendor/Bootstrap/Css/bootstrap.min.css');
const outputFile = path.join(themeDir, 'Assets/Vendor/Bootstrap/Css/bootstrap-subset.min.css');
// Verificar que existe el archivo de entrada
if (!fs.existsSync(inputFile)) {
console.error('ERROR: bootstrap.min.css not found at:', inputFile);
process.exit(1);
}
const inputSize = fs.statSync(inputFile).size;
console.log(`Input: bootstrap.min.css (${(inputSize / 1024).toFixed(2)} KB)`);
// Encontrar archivos PHP y JS manualmente
console.log('\nScanning for PHP and JS files...');
const patterns = [
'*.php',
'Public/**/*.php',
'Admin/**/*.php',
'Inc/**/*.php',
'Shared/**/*.php',
'template-parts/**/*.php',
'Assets/js/**/*.js',
];
let contentFiles = [];
for (const pattern of patterns) {
const files = globSync(pattern, { cwd: themeDir, absolute: true });
contentFiles = contentFiles.concat(files);
}
console.log(`Found ${contentFiles.length} files to analyze`);
if (contentFiles.length === 0) {
console.error('ERROR: No content files found. Check glob patterns.');
process.exit(1);
}
// Mostrar algunos archivos encontrados
console.log('\nSample files:');
contentFiles.slice(0, 5).forEach(f => console.log(' -', path.relative(themeDir, f)));
if (contentFiles.length > 5) {
console.log(` ... and ${contentFiles.length - 5} more`);
}
try {
const purgeCSSResult = await new PurgeCSS().purge({
css: [inputFile],
content: contentFiles,
// Safelist: Clases que SIEMPRE deben incluirse
safelist: {
standard: [
// Estados de navbar scroll (JavaScript)
'scrolled',
'navbar-scrolled',
// Bootstrap Collapse (JavaScript)
'show',
'showing',
'hiding',
'collapse',
'collapsing',
// Estados de dropdown
'dropdown-menu',
'dropdown-item',
'dropdown-toggle',
// Estados de form
'is-valid',
'is-invalid',
'was-validated',
// Visually hidden (accesibilidad)
'visually-hidden',
'visually-hidden-focusable',
// Screen reader
'sr-only',
// Container
'container',
'container-fluid',
// Row
'row',
// Display
'd-flex',
'd-none',
'd-block',
'd-inline-block',
'd-inline',
'd-grid',
// Common spacing
'mb-0', 'mb-1', 'mb-2', 'mb-3', 'mb-4', 'mb-5',
'mt-0', 'mt-1', 'mt-2', 'mt-3', 'mt-4', 'mt-5',
'me-0', 'me-1', 'me-2', 'me-3', 'me-4', 'me-5',
'ms-0', 'ms-1', 'ms-2', 'ms-3', 'ms-4', 'ms-5',
'mx-auto',
'py-0', 'py-1', 'py-2', 'py-3', 'py-4', 'py-5',
'px-0', 'px-1', 'px-2', 'px-3', 'px-4', 'px-5',
'p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5',
'gap-0', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-5',
'g-0', 'g-1', 'g-2', 'g-3', 'g-4', 'g-5',
// Flex
'flex-wrap',
'flex-nowrap',
'flex-column',
'flex-row',
'justify-content-center',
'justify-content-between',
'justify-content-start',
'justify-content-end',
'align-items-center',
'align-items-start',
'align-items-end',
// Text
'text-center',
'text-start',
'text-end',
'text-white',
'text-muted',
'fw-bold',
'fw-normal',
'small',
// Images
'img-fluid',
// Border/rounded
'rounded',
'rounded-circle',
'border',
'border-0',
// Shadow
'shadow',
'shadow-sm',
'shadow-lg',
// Width
'w-100',
'w-auto',
'h-100',
'h-auto',
// Toast classes (plugin IP View Limit)
'toast-container',
'toast',
'toast-body',
'position-fixed',
'bottom-0',
'end-0',
'start-50',
'translate-middle-x',
'text-dark',
'bg-warning',
'btn-close',
'm-auto',
],
deep: [
// Grid responsive
/^col-/,
/^col$/,
// Display responsive
/^d-[a-z]+-/,
// Navbar responsive
/^navbar-expand/,
/^navbar-/,
// Responsive margins/padding
/^m[tbsexy]?-[a-z]+-/,
/^p[tbsexy]?-[a-z]+-/,
// Text responsive
/^text-[a-z]+-/,
// Flex responsive
/^flex-[a-z]+-/,
/^justify-content-[a-z]+-/,
/^align-items-[a-z]+-/,
],
greedy: [
// Form controls
/form-/,
/input-/,
// Buttons
/btn/,
// Cards
/card/,
// Navbar
/navbar/,
/nav-/,
// Tables
/table/,
// Alerts
/alert/,
// Toast
/toast/,
// Badges
/badge/,
// Lists
/list-/,
],
},
// Mantener variables CSS de Bootstrap
variables: true,
// Mantener keyframes
keyframes: true,
// Mantener font-face
fontFace: true,
});
if (purgeCSSResult.length === 0 || !purgeCSSResult[0].css) {
console.error('ERROR: PurgeCSS returned empty result');
process.exit(1);
}
// POST-PROCESAMIENTO: Remover propiedades que son manejadas por CSS crítico
// Esto evita que bootstrap-subset.min.css sobrescriba los estilos críticos
let processedCSS = purgeCSSResult[0].css;
// Remover position:relative de .navbar (manejado por CriticalCSSService con sticky)
// Regex: encuentra .navbar{...position:relative...} y remueve solo position:relative
processedCSS = processedCSS.replace(
/\.navbar\{([^}]*?)position:relative;?([^}]*)\}/g,
'.navbar{$1$2}'
);
// Limpiar posibles dobles punto y coma o punto y coma antes de }
processedCSS = processedCSS.replace(/;;+/g, ';');
processedCSS = processedCSS.replace(/;\}/g, '}');
console.log('\nPost-processing: Removed position:relative from .navbar (handled by CriticalCSSService)');
// Agregar header al CSS generado
const header = `/**
* Bootstrap 5.3.2 Subset - ROI Theme
*
* Generado automáticamente con PurgeCSS
* Contiene SOLO las clases Bootstrap usadas en el tema.
*
* Original: ${(inputSize / 1024).toFixed(2)} KB
* Subset: ${(processedCSS.length / 1024).toFixed(2)} KB
* Reduccion: ${(100 - (processedCSS.length / inputSize * 100)).toFixed(1)}%
*
* Generado: ${new Date().toISOString()}
*
* Para regenerar:
* node build-bootstrap-subset.js
*/
`;
const outputCSS = header + processedCSS;
// Escribir archivo
fs.writeFileSync(outputFile, outputCSS);
const outputSize = fs.statSync(outputFile).size;
const reduction = ((1 - outputSize / inputSize) * 100).toFixed(1);
console.log('');
console.log('SUCCESS!');
console.log('-'.repeat(60));
console.log(`Output: bootstrap-subset.min.css (${(outputSize / 1024).toFixed(2)} KB)`);
console.log(`Reduction: ${reduction}% smaller`);
console.log('-'.repeat(60));
console.log('');
console.log('Next steps:');
console.log('1. Update Inc/enqueue-scripts.php to use bootstrap-subset.min.css');
console.log('2. Test the theme thoroughly');
console.log('3. Run PageSpeed Insights to verify improvement');
} catch (error) {
console.error('ERROR:', error.message);
console.error(error.stack);
process.exit(1);
}
}
buildBootstrapSubset();