diff --git a/.githooks/post-receive b/.githooks/post-receive index babe815..cf19a35 100644 --- a/.githooks/post-receive +++ b/.githooks/post-receive @@ -22,31 +22,75 @@ } deploy_expressjs_blog() { - local ref="$1" - local custom_path="${2:-}" - local custom_env="${3:-}" + local oldrev="$1" + local newrev="$2" + local ref="$3" + local custom_path="${4:-}" + local custom_env="${5:-}" local branch="${ref#refs/heads/}" + local path="${custom_path:-/srv/jasonpoage.com/expressjs-blog-$branch}" local envfile="${custom_env:-/srv/jasonpoage.com/$branch.env}" set -x - GIT_DIR="/srv/jasonpoage.com/expressjs-blog.git" + # Define the bare main application repository (used explicitly when needed) + local MAIN_APP_BARE_REPO="/srv/jasonpoage.com/expressjs-blog.git" + # Define the bare content repository (used explicitly when needed) + local CONTENT_BARE_REPO="/srv/jasonpoage.com/expressjs-blog-posts.git" if [[ "$branch" == "production" || "$branch" == "main" || "$branch" == "testing" ]]; then GIT_WORK_TREE="$path" git checkout -f "$branch" - cd "$path" || return 1 + cd "$path" || { + echo "Error: Could not change directory to $path" + return 1 + } [[ -z "$custom_path" ]] && ln -f "$envfile" "$path/.env" || { cp -f "$envfile" "$path/.env" } - yarn - yarn combine:css + + local install_required=true + if [[ -z "$custom_path" && -n "$oldrev" && "$oldrev" != "0000000000000000000000000000000000000000" ]]; then + # This block only runs for actual deployments (not test runs) + # and if it's not the initial clone (oldrev is not all zeros) + + # Use git diff-tree to check if package.json or yarn.lock changed between old and new revision + # --name-only shows only file names + # -r makes it recursive + # $oldrev..$newrev compares the commits + if ! git --git-dir="$MAIN_APP_BARE_REPO" diff-tree --name-only -r "$oldrev..$newrev" | grep -qE "(package\.json|yarn\.lock)$"; then + echo "No changes detected in package.json or yarn.lock. Skipping yarn install." + install_required=false + else + echo "Changes detected in package.json or yarn.lock. Running yarn install." + fi + else + echo "Running yarn install (initial deployment or test environment)." + fi + + if [[ "$install_required" == "true" ]]; then + yarn + fi + + # This script checks if css bundle needs updated, and updates it + if [[ -z "$custom_path" ]]; then + # For live deployments, pass the oldrev, newrev, and MAIN_APP_BARE_REPO + yarn combine:css "$oldrev" "$newrev" "$MAIN_APP_BARE_REPO" + else + # For temporary test environments, run without Git comparison arguments. + # This will trigger the "No Git revisions provided. Forcing full CSS bundling." path in combine-css.js + yarn combine:css + fi + [[ -z "$custom_path" ]] && systemctl --user restart express-blog@"$branch".service - if [[ -d "$path/content" ]]; then - cd "$path/content" || return 1 + if [[ -d "$path/content" && -d "$path/content" ]]; then + cd "$path/content" || { + echo "Error: Could not change directory to $path/content" + return 1 + } current_commit=$(git rev-parse HEAD) git fetch origin latest_commit=$(git rev-parse origin/main) @@ -58,6 +102,8 @@ echo "Content already up to date" fi return 0 + else + git clone --branch main "$CONTENT_BARE_REPO" "$path/content" fi local force_refresh="${4:-false}" if [[ "$force_refresh" == "true" ]] || [[ ! -d "$path/content" ]]; then @@ -73,11 +119,11 @@ tmpdir=$(mktemp -d) pidfile="$tmpdir/test.pid" logfile="$tmpdir/test.log" - trap "kill $(cat "$pidfile" 2>/dev/null) 2>/dev/null; rm -rf \"$tmpdir\"" EXIT + trap "kill $(cat "$pidfile" 2>/dev/null) 2>/dev/null || true; rm -rf \"$tmpdir\"" EXIT git clone . "$tmpdir" --quiet --branch "$branch" - deploy_expressjs_blog "refs/heads/$branch" "$tmpdir" "/srv/jasonpoage.com/${branch}.env" + deploy_expressjs_blog "" "" "refs/heads/$branch" "$tmpdir" "/srv/jasonpoage.com/${branch}.env" cd "$tmpdir" || return 1 export TEST_PORT=4123 @@ -94,18 +140,18 @@ fi if ! npm run test:postreceive; then - kill "$(cat "$pidfile")" 2>/dev/null + kill "$(cat "$pidfile")" 2>/dev/null || true cat "$logfile" return 1 fi - kill "$(cat "$pidfile")" 2>/dev/null + kill "$(cat "$pidfile")" 2>/dev/null || true } while read -r oldrev newrev ref; do branch="${ref#refs/heads/}" if run_postreceive_tests "$branch"; then - deploy_expressjs_blog "$ref" + deploy_expressjs_blog "$oldrev" "$newrev" "$ref" else echo "Post-receive tests failed. Deployment aborted." exit 1 diff --git a/build-static.js b/build-static.js deleted file mode 100644 index 75f6d28..0000000 --- a/build-static.js +++ /dev/null @@ -1,101 +0,0 @@ -// build-static.js -const fs = require("fs").promises; -const path = require("path"); -const matter = require("gray-matter"); -const { marked } = require("marked"); -const handlebars = require("handlebars"); - -const postsDir = path.join(__dirname, "posts"); -const outputDir = path.join(__dirname, "static"); - -// Load and compile templates -async function loadTemplate(name) { - const file = await fs.readFile( - path.join(__dirname, "src", "views", "pages", `${name}.handlebars`), - "utf8" - ); - return handlebars.compile(file); -} - -async function getAllPosts() { - const years = await fs.readdir(postsDir); - let posts = []; - - for (const year of years) { - const yearDir = path.join(postsDir, year); - const months = await fs.readdir(yearDir); - - for (const month of months) { - const monthDir = path.join(yearDir, month); - const files = await fs.readdir(monthDir); - - for (const file of files) { - if (!file.endsWith(".md")) continue; - const filePath = path.join(monthDir, file); - const contentRaw = await fs.readFile(filePath, "utf8"); - const { data: frontmatter, content } = matter(contentRaw); - posts.push({ - year, - month, - name: path.basename(file, ".md"), - frontmatter, - content, - }); - } - } - } - - return posts; -} - -async function build() { - // Prepare output directories - await fs.rm(outputDir, { recursive: true, force: true }); - await fs.mkdir(outputDir, { recursive: true }); - - // Load templates - const postTemplate = await loadTemplate("post"); - const homeTemplate = await loadTemplate("home"); - const errorTemplate = await loadTemplate("error"); - - // Build posts pages - const posts = await getAllPosts(); - - for (const post of posts) { - const htmlContent = marked(post.content); - const context = { - title: post.frontmatter.title, - date: post.frontmatter.date, - author: post.frontmatter.author, - content: htmlContent, - }; - - const html = postTemplate(context); - const outDir = path.join(outputDir, "post", post.year, post.month); - await fs.mkdir(outDir, { recursive: true }); - await fs.writeFile(path.join(outDir, `${post.name}.html`), html, "utf8"); - } - - // Build home page - const homeContext = { - title: "Blog Home", - content: "Welcome to the blog.", - }; - const homeHtml = homeTemplate(homeContext); - await fs.writeFile(path.join(outputDir, "index.html"), homeHtml, "utf8"); - - // Optionally build error page - const errorContext = { - statusCode: 404, - title: "Not Found", - message: "The requested blog post could not be found.", - content: "", - }; - const errorHtml = errorTemplate(errorContext); - await fs.writeFile(path.join(outputDir, "404.html"), errorHtml, "utf8"); -} - -build().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/combine-css.js b/combine-css.js deleted file mode 100644 index 6208d23..0000000 --- a/combine-css.js +++ /dev/null @@ -1,55 +0,0 @@ -// combine-css.js -const fs = require("fs"); -const path = require("path"); -const postcss = require("postcss"); -const atImport = require("postcss-import"); - -// Define your main CSS entry point file -// This is the file that contains all your @import statements. -const mainCssEntry = path.join(__dirname, "src", "css", "styles.css"); - -// Define the output path and filename for the combined CSS -const outputDir = path.join(__dirname, "public", "css"); -const outputFilename = "styles.css"; -const outputPath = path.join(outputDir, outputFilename); - -async function combineCssImports() { - console.log(`Starting CSS bundling from: ${mainCssEntry}`); - - try { - // Read the content of the main CSS file - const cssContent = fs.readFileSync(mainCssEntry, "utf8"); - - // Process with PostCSS and postcss-import plugin - const result = await postcss([ - atImport({ - // This 'path' option tells postcss-import where to look for imported files. - // It's crucial for resolving relative paths like "@import './components/header.css';" - path: [path.join(__dirname, "src", "css")], - }), - ]).process(cssContent, { - from: mainCssEntry, // Tell PostCSS the original file path - to: outputPath, // Tell PostCSS the output file path (useful for source maps, etc.) - }); - - // Ensure the output directory exists - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - // Write the combined CSS to the output file - fs.writeFileSync(outputPath, result.css); - console.log(`\nSuccessfully combined all imported CSS into: ${outputPath}`); - } catch (error) { - console.error("Error combining CSS files:", error); - if (error.file) { - console.error(`Error in file: ${error.file}`); - console.error( - `At line ${error.line}, column ${error.column}: ${error.reason}` - ); - } - process.exit(1); // Exit with an error code - } -} - -combineCssImports(); diff --git a/content b/content index 0e20b80..239c1cf 160000 --- a/content +++ b/content @@ -1 +1 @@ -Subproject commit 0e20b801a651c86a62341188342f4e42e2b104e1 +Subproject commit 239c1cfcd1785c1deb6d69b2eb34f475c8bcfab4 diff --git a/package.json b/package.json index e210a56..5eaba99 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "combine:css": "node combine-css.js", + "combine:css": "node scripts/combine-css.js", "start": "nodemon ./src/app.js --trace-exit", "maildev": "maildev", "main": "pm2 start ecosystem.config.js --only expressjs-blog-main", diff --git a/scripts/build-static.js b/scripts/build-static.js new file mode 100644 index 0000000..75f6d28 --- /dev/null +++ b/scripts/build-static.js @@ -0,0 +1,101 @@ +// build-static.js +const fs = require("fs").promises; +const path = require("path"); +const matter = require("gray-matter"); +const { marked } = require("marked"); +const handlebars = require("handlebars"); + +const postsDir = path.join(__dirname, "posts"); +const outputDir = path.join(__dirname, "static"); + +// Load and compile templates +async function loadTemplate(name) { + const file = await fs.readFile( + path.join(__dirname, "src", "views", "pages", `${name}.handlebars`), + "utf8" + ); + return handlebars.compile(file); +} + +async function getAllPosts() { + const years = await fs.readdir(postsDir); + let posts = []; + + for (const year of years) { + const yearDir = path.join(postsDir, year); + const months = await fs.readdir(yearDir); + + for (const month of months) { + const monthDir = path.join(yearDir, month); + const files = await fs.readdir(monthDir); + + for (const file of files) { + if (!file.endsWith(".md")) continue; + const filePath = path.join(monthDir, file); + const contentRaw = await fs.readFile(filePath, "utf8"); + const { data: frontmatter, content } = matter(contentRaw); + posts.push({ + year, + month, + name: path.basename(file, ".md"), + frontmatter, + content, + }); + } + } + } + + return posts; +} + +async function build() { + // Prepare output directories + await fs.rm(outputDir, { recursive: true, force: true }); + await fs.mkdir(outputDir, { recursive: true }); + + // Load templates + const postTemplate = await loadTemplate("post"); + const homeTemplate = await loadTemplate("home"); + const errorTemplate = await loadTemplate("error"); + + // Build posts pages + const posts = await getAllPosts(); + + for (const post of posts) { + const htmlContent = marked(post.content); + const context = { + title: post.frontmatter.title, + date: post.frontmatter.date, + author: post.frontmatter.author, + content: htmlContent, + }; + + const html = postTemplate(context); + const outDir = path.join(outputDir, "post", post.year, post.month); + await fs.mkdir(outDir, { recursive: true }); + await fs.writeFile(path.join(outDir, `${post.name}.html`), html, "utf8"); + } + + // Build home page + const homeContext = { + title: "Blog Home", + content: "Welcome to the blog.", + }; + const homeHtml = homeTemplate(homeContext); + await fs.writeFile(path.join(outputDir, "index.html"), homeHtml, "utf8"); + + // Optionally build error page + const errorContext = { + statusCode: 404, + title: "Not Found", + message: "The requested blog post could not be found.", + content: "", + }; + const errorHtml = errorTemplate(errorContext); + await fs.writeFile(path.join(outputDir, "404.html"), errorHtml, "utf8"); +} + +build().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/combine-css.js b/scripts/combine-css.js new file mode 100644 index 0000000..b4d028a --- /dev/null +++ b/scripts/combine-css.js @@ -0,0 +1,127 @@ +// combine-css.js +const fs = require("fs"); +const path = require("path"); +const postcss = require("postcss"); +const atImport = require("postcss-import"); + +// Define your main CSS entry point file +// This is the file that contains all your @import statements. +const mainCssEntry = path.join(__dirname, "..", "src", "css", "styles.css"); + +// Define the output path and filename for the combined CSS +const outputDir = path.join(__dirname, "..", "public", "css"); +const outputFilename = "styles.css"; +const outputPath = path.join(outputDir, "..", outputFilename); + +async function combineCssImports() { + console.log(`Starting CSS bundling from: ${mainCssEntry}`); + + try { + // Read the content of the main CSS file + const cssContent = fs.readFileSync(mainCssEntry, "utf8"); + + // Process with PostCSS and postcss-import plugin + const result = await postcss([ + atImport({ + // This 'path' option tells postcss-import where to look for imported files. + // It's crucial for resolving relative paths like "@import './components/header.css';" + path: [path.join(__dirname, "src", "css")], + }), + ]).process(cssContent, { + from: mainCssEntry, // Tell PostCSS the original file path + to: outputPath, // Tell PostCSS the output file path (useful for source maps, etc.) + }); + + // Ensure the output directory exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Write the combined CSS to the output file + fs.writeFileSync(outputPath, result.css); + console.log(`\nSuccessfully combined all imported CSS into: ${outputPath}`); + } catch (error) { + console.error("Error combining CSS files:", error); + if (error.file) { + console.error(`Error in file: ${error.file}`); + console.error( + `At line ${error.line}, column ${error.column}: ${error.reason}` + ); + } + process.exit(1); // Exit with an error code + } +} +/** + * Main execution logic for the script. + * This function decides whether to call combineCssImports. + */ +async function main() { + // Extract arguments passed from the shell script + // process.argv[0] is 'node', process.argv[1] is 'combine-css.js' + // So, actual arguments start from index 2 + const oldRev = process.argv[2]; + const newRev = process.argv[3]; + const gitDir = process.argv[4]; + + // Condition 1: No arguments passed, force regeneration. + if (!oldRev && !newRev && !gitDir) { + console.log("No Git revisions provided. Forcing full CSS bundling."); + await combineCssImports(); + return; + } + + // Condition 2: Git revisions are provided, check for updates. + if (oldRev && newRev && gitDir) { + // oldRev "0" means it's an initial push or branch creation, so always bundle + if (oldRev === "0000000000000000000000000000000000000000") { + console.log( + "Initial push (old revision is zero). Forcing full CSS bundling." + ); + await combineCssImports(); + return; + } + + try { + // Construct the Git command to check for CSS file changes + const gitCommand = `git --git-dir="${gitDir}" diff-tree --name-only -r ${oldRev}..${newRev}`; + console.log( + `Running Git command to check for CSS changes: ${gitCommand}` + ); + + const changedFiles = execSync(gitCommand, { encoding: "utf8" }).trim(); + + // Check if any changed file matches our CSS pattern + const cssChangeDetected = changedFiles + .split("\n") + .some((file) => file.match(/^src\/css\/.*\.css$/)); + + if (!cssChangeDetected) { + console.log( + "No changes detected in CSS files (src/css/*.css). Skipping CSS bundling." + ); + return; // Exit without calling combineCssImports + } else { + console.log( + "Changes detected in CSS files. Proceeding with CSS bundling." + ); + await combineCssImports(); + } + } catch (gitError) { + // If git command fails (e.g., bad revisions, repo not found), log and proceed with bundling to be safe. + console.warn( + `Warning: Git diff check failed (${gitError.message}). Proceeding with CSS bundling to be safe.` + ); + await combineCssImports(); // Fallback to bundling if the check fails + } + return; + } + + // Fallback for unexpected argument combinations (e.g., only oldRev provided) + console.warn( + "Warning: Unexpected arguments provided to script. Proceeding with CSS bundling to be safe." + ); + await combineCssImports(); +} + +// Call the main execution function +main(); diff --git a/src/routes/admin.js b/src/routes/admin.js index 0cac580..ed025fc 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -40,7 +40,7 @@ res .status(301) .set("Location", adminLoginUrl) - .render("pages/redirect", { layout: "layouts/redirect", adminLoginUrl }); + .render("pages/redirect", { layout: "redirect", adminLoginUrl }); }); module.exports = router;