feat: migrate Hugo Bootstrap theme to latest Hugo with Tailwind CSS and refactor codebase

* replace Bootstrap-based styling with Tailwind CSS
* update theme compatibility for latest Hugo version
* refactor templates and partials
* fix outdated code and broken components
* improve project structure and maintainability
* optimize styling and frontend build setup
This commit is contained in:
Al Murad Uzzaman
2026-05-10 13:38:01 +06:00
parent eac3f49bc5
commit f8b297eaad
233 changed files with 5272 additions and 9256 deletions

217
scripts/themeGenerator.js Normal file
View File

@@ -0,0 +1,217 @@
const fs = require("fs");
const path = require("path");
/**
* Determine the paths based on setup mode (theme vs project)
* @returns {Object} Configuration object with paths and mode info
*/
function determinePaths() {
const rootHugoToml = path.join(__dirname, "../hugo.toml");
const exampleSiteHugoToml = path.join(__dirname, "../exampleSite/hugo.toml");
if (fs.existsSync(exampleSiteHugoToml)) {
// Theme setup mode - exampleSite structure exists
return {
hugoTomlPath: exampleSiteHugoToml,
themePath: path.join(__dirname, "../exampleSite/data/theme.json"),
outputPath: path.join(__dirname, "../assets/css/generated-theme.css"),
isThemeSetup: true,
};
} else if (fs.existsSync(rootHugoToml)) {
// Project setup mode - hugo.toml at root
try {
const hugoTomlContent = fs.readFileSync(rootHugoToml, "utf8");
const themeNameMatch = hugoTomlContent.match(
/^theme\s*=\s*["'\[]?"?([^"'\]]+)"?[\]"]?/m,
);
if (!themeNameMatch || !themeNameMatch[1]) {
throw new Error("Could not extract theme name from hugo.toml");
}
const themeName = themeNameMatch[1];
return {
hugoTomlPath: rootHugoToml,
themePath: path.join(__dirname, "../data/theme.json"),
outputPath: path.join(
__dirname,
`../themes/${themeName}/assets/css/generated-theme.css`,
),
isThemeSetup: false,
};
} catch (error) {
throw new Error(`Failed to determine paths: ${error.message}`);
}
} else {
throw new Error(
"Could not determine setup mode: neither exampleSite/hugo.toml nor root hugo.toml found",
);
}
}
const { themePath, outputPath } = determinePaths();
// Helper to convert color name from snake_case to kebab-case
const toKebab = (str) => str.replace(/_/g, "-");
// Helper to extract a clean font name
const findFont = (fontStr) =>
fontStr.replace(/\+/g, " ").replace(/:[^:]+/g, "");
/**
* Add color entries to CSS array
* @param {Array} cssLines - Array of CSS lines to append to
* @param {Object} colors - Color object to process
* @param {string} prefix - Optional prefix for color variable names
*/
function addColorsToCss(cssLines, colors, prefix = "") {
Object.entries(colors).forEach(([key, value]) => {
const colorName = prefix
? `--color-${prefix}-${toKebab(key)}`
: `--color-${toKebab(key)}`;
cssLines.push(` ${colorName}: ${value};`);
});
}
/**
* Generate theme CSS from theme.json configuration
* @throws {Error} If theme.json is missing or invalid
*/
function generateThemeCSS() {
// Validate that theme.json exists
if (!fs.existsSync(themePath)) {
throw new Error(`Theme configuration not found: ${themePath}`);
}
try {
// Read and parse theme configuration
const themeConfig = JSON.parse(fs.readFileSync(themePath, "utf8"));
// Validate required theme structure
if (!themeConfig.colors || !themeConfig.fonts) {
throw new Error(
"Invalid theme.json: missing 'colors' or 'fonts' section",
);
}
// Build CSS using array for better performance
const cssLines = [
"/**",
' * Auto-generated from "data/theme.json"',
" * DO NOT EDIT THIS FILE MANUALLY",
" * Run: node scripts/themeGenerator.js",
" */",
"",
"@theme {",
" /* === Colors === */",
];
// Add default theme colors
if (themeConfig.colors.default?.theme_color) {
addColorsToCss(cssLines, themeConfig.colors.default.theme_color);
}
// Add default text colors
if (themeConfig.colors.default?.text_color) {
addColorsToCss(cssLines, themeConfig.colors.default.text_color);
}
// Add darkmode colors (if available)
if (themeConfig.colors.darkmode) {
cssLines.push("", " /* === Darkmode Colors === */");
if (themeConfig.colors.darkmode.theme_color) {
addColorsToCss(
cssLines,
themeConfig.colors.darkmode.theme_color,
"darkmode",
);
}
if (themeConfig.colors.darkmode.text_color) {
addColorsToCss(
cssLines,
themeConfig.colors.darkmode.text_color,
"darkmode",
);
}
}
// Add font families
cssLines.push("", " /* === Font Families === */");
const fontFamily = themeConfig.fonts.font_family || {};
Object.entries(fontFamily)
.filter(([key]) => !key.includes("type"))
.forEach(([key, font]) => {
const fontFallback = fontFamily[`${key}_type`] || "sans-serif";
const fontValue = `${findFont(font)}, ${fontFallback}`;
cssLines.push(` --font-${toKebab(key)}: ${fontValue};`);
});
// Add font sizes
cssLines.push("", " /* === Font Sizes === */");
const baseSize = Number(themeConfig.fonts.font_size?.base || 16);
const scale = Number(themeConfig.fonts.font_size?.scale || 1.25);
cssLines.push(` --text-base: ${baseSize}px;`);
cssLines.push(` --text-base-sm: ${baseSize * 0.8}px;`);
let currentSize = scale;
for (let i = 6; i >= 1; i--) {
cssLines.push(` --text-h${i}: ${currentSize}rem;`);
cssLines.push(` --text-h${i}-sm: ${currentSize * 0.9}rem;`);
currentSize *= scale;
}
cssLines.push("}");
// Ensure output directory exists
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Write the file
fs.writeFileSync(outputPath, cssLines.join("\n") + "\n");
console.log("✅ Theme CSS generated successfully at:", outputPath);
} catch (error) {
throw new Error(`Failed to generate theme CSS: ${error.message}`);
}
}
// Generate CSS on startup
try {
generateThemeCSS();
} catch (error) {
console.error("❌ Error:", error.message);
process.exit(1);
}
// Check for --watch flag
if (process.argv.includes("--watch")) {
let debounceTimer;
const watcher = fs.watch(themePath, (eventType) => {
if (eventType === "change") {
// Debounce to avoid multiple triggers
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
try {
generateThemeCSS();
} catch (error) {
console.error("❌ Error regenerating theme CSS:", error.message);
}
}, 300);
}
});
// Handle graceful shutdown
process.on("SIGINT", () => {
clearTimeout(debounceTimer);
watcher.close();
console.log("\n👋 Watcher stopped");
process.exit(0);
});
console.log("👁️ Watching for changes to:", themePath);
}