diff --git a/.githooks/post-receive b/.githooks/post-receive index c509e99..009704c 100644 --- a/.githooks/post-receive +++ b/.githooks/post-receive @@ -14,49 +14,64 @@ } get_log_file_path() { - echo "/srv/jasonpoage.com/logs/post-receive-${TIMESTAMP}.log" + echo "/srv/jasonpoage.com/logs/receive-${TIMESTAMP}.log" +} + +copy_env_file() { + local branch="$1" + local env_file + env_file="$(get_deploy_env_file "$branch")" + source "$env_file" + # Copy the .env file for the test environment + cp "$env_file" "$tmpdir/.env" || { + echo "Error: Failed to copy .env file for test environment." + return 1 + } +} + +clone_branch() { + local branch="$1" + local tmpdir="$2" + # This creates a proper working tree at $tmpdir, which is essential for submodules. + git clone --branch "$branch" "$GIT_DIR" "$tmpdir" || { + echo "Error: Failed to clone main application for tests to $tmpdir" + return 1 + } +} + +# Function to wait for a service to become available +_wait_for_service() { + local url="$1" + local timeout=30 # seconds + local start_time + start_time=$(date +%s) + echo "Waiting for service at $url to become available (timeout: ${timeout}s)..." + while :; do + if curl --silent --fail "$url" >/dev/null; then + echo "Service at $url is up!" + return 0 + fi + current_time=$(date +%s) + if ((current_time - start_time >= timeout)); then + echo "Error: Service at $url did not become available within ${timeout}s." + return 1 + fi + sleep 1 + done } quick_deploy_expressjs_blog() { local branch="$1" - local deploy_path="$2" - local envfile="$3" - echo "Performing quick deployment for '$branch' to '$deploy_path'..." - - # Remove old deployment and create fresh one + local test_dir="$2" + local deploy_path="$3" + local envfile="$4" + echo "Moving tested deployment from '$test_dir' to '$deploy_path'..." [[ -d "$deploy_path" ]] && rm -rf "$deploy_path" - - # Clone the latest version - git clone --branch "$branch" "$GIT_DIR" "$deploy_path" || { - echo "Error: Failed to clone branch $branch to $deploy_path" - return 1 - } - - cd "$deploy_path" || { - echo "Error: Could not change directory to $deploy_path" - return 1 - } - - # Update submodules - git submodule update --init --recursive || { - echo "Error: Failed to initialize submodules" - return 1 - } - - # Set up environment file + mv "$test_dir" "$deploy_path" + git config -f /srv/jasonpoage.com/expressjs-blog.git/modules/content/config core.worktree "/srv/jasonpoage.com/expressjs-blog-testing/content" ln -f "$envfile" "$deploy_path/.env" - - # Install dependencies and build - yarn - yarn --production=false combine:css || { - echo "Error: CSS combination failed" - return 1 - } - # Restart the service systemctl --user restart express-blog@"$branch".service - - echo "Quick deployment complete for branch '$branch'" } # Smart dependency installation (only if package files changed) @@ -64,7 +79,6 @@ local oldrev="$1" local newrev="$2" local install_required=true - if [[ -n "$oldrev" && "$oldrev" != "0000000000000000000000000000000000000000" ]]; then if ! git --git-dir="$GIT_DIR" 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." @@ -92,10 +106,11 @@ echo "Error: Could not change directory to $deploy_path" return 1 } - SAVED_GIT_DIR="$GIT_DIR" unset GIT_DIR + git remote show + git branch -l # Update main repository - always fetch and hard reset (handles force pushes) echo "Fetching and updating main repository..." git fetch origin || { @@ -110,7 +125,7 @@ # Update submodules - handle force pushes by resetting each submodule echo "Updating submodules..." - git submodule update --init --recursive --force || { + git submodule update --init --recursive --force || { echo "Standard submodule update failed, cleaning and retrying..." return 1 } @@ -121,7 +136,7 @@ unset SAVED_GIT_DIR } -# Conservative deployment function for production environments +# Updated conservative deployment function using the unified update conservative_deploy_expressjs_blog() { local branch="$1" local oldrev="${2:-}" @@ -129,7 +144,9 @@ local deploy_path="${4:-}" local envfile="${5:-}" - echo "Using conservative deployment method for production environment..." + echo "Using preserving deployment method for production environment..." + + # set -x # Initialize the deployment directory if it doesn't exist if [[ ! -d "$deploy_path" ]]; then @@ -140,75 +157,166 @@ } fi - # Update repository and submodules - update_repo_and_submodules "$branch" "$deploy_path" || return 1 + # Single unified update function handles everything + update_repo_and_submodules "$branch" "$deploy_path" "$GIT_DIR" || return 1 cd "$deploy_path" || { echo "Error: Could not change directory to $deploy_path" return 1 } - # Set up environment file ln -f "$envfile" "$deploy_path/.env" || return 1 - # Install dependencies if needed get_dependencies "$oldrev" "$newrev" || return 1 - # Build CSS yarn combine:css "$oldrev" "$newrev" "$GIT_DIR" || return 1 - # Restart service systemctl --user restart express-blog@"$branch".service - echo "Conservative deployment complete for branch '$branch'" + # set +x } deploy_expressjs_blog() { local branch="$1" - local oldrev="${2:-}" - local newrev="${3:-}" + local test_dir="$2" + local oldrev="${3:-}" + local newrev="${4:-}" # Define deployment path and env file path local deploy_path envfile deploy_path=$(get_deploy_path "$branch") envfile=$(get_deploy_env_file "$branch") + case "$branch" in "testing") - quick_deploy_expressjs_blog "$branch" "$deploy_path" "$envfile" - ;; - "staging"|"main") - # For staging/production, use the preserving method (keep logs, update in place) - conservative_deploy_expressjs_blog "$branch" "$oldrev" "$newrev" "$deploy_path" "$envfile" - ;; - *) - echo "Branch '$branch' not configured for deployment. Skipping." + quick_deploy_expressjs_blog "$branch" "$test_dir" "$deploy_path" "$envfile" return 0 - ;; + ;; + "staging") + # For production/main, use the preserving method (keep logs, update in place) + conservative_deploy_expressjs_blog "$branch" "$oldrev" "$newrev" "$deploy_path" "$envfile" + return 0 + ;; + "main") + conservative_deploy_expressjs_blog "$branch" "$oldrev" "$newrev" "$deploy_path" "$envfile" + # echo "Deployment available at: $deploy_path" + ;; + esac } -# Post-receive hook: Deploy the changes that were just pushed -while read -r oldrev newrev ref; do - branch="${ref#refs/heads/}" - - echo "--- Post-receive deployment for branch: $branch (from $oldrev to $newrev) ---" - - # Only deploy branches we care about - case "$branch" in - "testing"|"staging"|"main") - if deploy_expressjs_blog "$branch" "$oldrev" "$newrev"; then - echo "Deployment of $branch complete." - else - echo "Deployment of $branch failed." - # Note: We don't exit 1 here because the push has already been accepted - # Log the failure but continue processing other branches - fi - ;; - *) - echo "Branch '$branch' not configured for deployment. Skipping." - ;; - esac -done -echo "Post-receive processing complete." +run_postreceive_tests() { + local branch="$1" + local tmpdir pidfile logfile + tmpdir="$2" + pidfile="$tmpdir/test.pid" + logfile=$(get_log_file_path) + # Trap to ensure cleanup even if script exits unexpectedly + trap "kill \"$(cat "$pidfile" 2>/dev/null)\" 2>/dev/null || true; rm -f \"$pidfile\"" EXIT + + echo "Running post-receive tests for branch '$branch' in temporary environment." + + clone_branch "$branch" "$tmpdir" || return 1 + + cd "$tmpdir" || { + echo "Error: Could not change directory to $tmpdir" + return 1 + } + + copy_env_file "$branch" || return 1 + + initialize_submodules "$tmpdir" || return 1 + + # echo "Skipping tests" + # return 0 + + echo "Running build scripts..." + yarn + combine_css || return 1 + + echo "Starting application for tests..." + systemctl --user stop express-blog@"$branch".service + nohup node src/app.js >>"$logfile" 2>&1 & + echo $! >"$pidfile" + + # set +x + wait_for_service "$logfile" + # set -x + + echo "Running tests..." + run_tests "$branch" "$pidfile" "$logfile" || return 1 + + kill "$(cat "$pidfile")" 2>/dev/null || true + + echo "Tests passed for branch '$branch' in temporary environment." + return 0 +} + +initialize_submodules() { + local worktree="$1" + echo "Initializing and updating submodules for test environment..." + git --git-dir="$worktree/.git" --work-tree="$worktree" submodule update --init --recursive || { + ls "$worktree" -a + echo "Error: Failed to initialize/update submodules for test environment." + return 1 + } +} +update_submodules() { + local worktree="$1" + git --work-tree="$worktree" submodule update --init --recursive || { + echo "Error: Failed to initialize/update submodules in deployment" + return 1 + } +} +wait_for_service() { + local logfile="$1" + # Wait for the application to become responsive + if ! _wait_for_service "$SERVER_SCHEMA://$SERVER_DOMAIN"; then + echo "Application did not start or respond for tests. Check logs in $logfile:" + cat "$logfile" # Display logs on failure + return 1 + fi +} + +run_tests() { + branch="$1" + pidfile="$2" + logfile="$3" + echo "Running npm tests..." + if ! npm run test:postreceive; then + kill "$(cat "$pidfile")" 2>/dev/null || true + echo "Tests failed for branch '$branch'. Application logs from $logfile:" + cat "$logfile" + return 1 + fi +} + +combine_css() { + yarn --production=false combine:css || { + echo "Error: yarn combine:css failed." + return 1 + } +} + +ssh-add -l + +set -x +# Main script execution loop +while read -r oldrev newrev ref; do + tmpdir=$(mktemp -d) + branch="${ref#refs/heads/}" + echo "--- Processing push for branch: $branch (from $oldrev to $newrev) ---" + if run_postreceive_tests "$branch" "$tmpdir"; then + echo "Tests passed for $branch. Proceeding with deployment." + deploy_expressjs_blog "$branch" "$tmpdir" "$oldrev" "$newrev" + + # deploy_expressjs_blog "$branch" "$tmpdir" + echo "Deployment of $branch complete." + else + echo "Post-receive tests failed for $branch. Deployment aborted." + exit 1 + fi +done +set +x diff --git a/content b/content index 2946645..b06e407 160000 --- a/content +++ b/content @@ -1 +1 @@ -Subproject commit 29466451374dd1ddd15fe4376225e90e6041e60c +Subproject commit b06e40705ee75e462bb82b090b01de225f80284e diff --git a/public/css/contact.css b/public/css/contact.css index 51ba0b1..7c7c349 100644 --- a/public/css/contact.css +++ b/public/css/contact.css @@ -126,14 +126,6 @@ pointer-events: none !important; } -/* -.char-counter { - text-align: right; - font-size: 0.9em; - color: #666; - margin-top: 5px; -} */ - .char-counter { text-align: right; font-size: 0.9em; diff --git a/public/css/docs-sidebar.css b/public/css/docs-sidebar.css index ce44650..8e3e4a2 100644 --- a/public/css/docs-sidebar.css +++ b/public/css/docs-sidebar.css @@ -1,5 +1,3 @@ - -/* Sidebar styling to match existing aesthetic */ .layout .sidebar { width: 250px; background-color: #f8f9fa; @@ -18,12 +16,12 @@ margin: 0; } -/* Documentation path items - like year blocks */ +/* Documentation path items */ .layout .docs-path-item { margin-bottom: 1.5rem; } -/* Path labels - styled like menu-year */ +/* Path labels */ .layout .docs-path-label { font-weight: 700; font-size: 1.2rem; diff --git a/public/css/page.css b/public/css/page.css index 44ad59b..232dfa7 100644 --- a/public/css/page.css +++ b/public/css/page.css @@ -22,24 +22,6 @@ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); border-radius: 12px 12px 0 0; } -/* -.markdown-content h1 { - font-size: 3.2rem; - color: #1a202c; - margin-bottom: 1rem; - line-height: 1.2; - text-align: center; - font-weight: 800; - letter-spacing: -0.03em; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - padding-bottom: 1.5rem; - border-bottom: 3px solid #e2e8f0; - margin-bottom: 2.5rem; -} */ - .markdown-content h1 { font-size: 1.9rem; color: #2d3748; @@ -186,7 +168,7 @@ width: 100%; } -/* Social media links special styling */ +/* Social media links */ .markdown-content ul li a { display: inline-flex; align-items: center; @@ -234,12 +216,6 @@ border-radius: 6px; } - /* .markdown-content h1 { - font-size: 2rem; - padding-bottom: 1rem; - margin-bottom: 1.5rem; - } */ - .markdown-content h1 { font-size: 1.4rem; margin-top: 2rem; @@ -265,10 +241,6 @@ padding: 1rem 0.8rem; } - /* .markdown-content h1 { - font-size: 1.8rem; - } */ - .markdown-content h1 { font-size: 1.3rem; } diff --git a/public/css/sitemap-xml.css b/public/css/sitemap-xml.css index 188f3ce..bd5ba48 100644 --- a/public/css/sitemap-xml.css +++ b/public/css/sitemap-xml.css @@ -1,37 +1,37 @@ body { -font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; -font-size: 16px; -margin: 30px; -background: #f9f9f9; -color: #222; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-size: 16px; + margin: 30px; + background: #f9f9f9; + color: #222; } table { -border-collapse: collapse; -width: 100%; -max-width: 900px; -margin: auto; -background: white; -box-shadow: 0 0 12px rgba(0,0,0,0.1); + border-collapse: collapse; + width: 100%; + max-width: 900px; + margin: auto; + background: white; + box-shadow: 0 0 12px rgba(0,0,0,0.1); } th, td { -border: 1px solid #ddd; -padding: 12px 15px; -text-align: left; + border: 1px solid #ddd; + padding: 12px 15px; + text-align: left; } th { -background-color: #007acc; -color: white; -font-weight: 600; + background-color: #007acc; + color: white; + font-weight: 600; } a { -color: #007acc; -text-decoration: none; + color: #007acc; + text-decoration: none; } a:hover { -text-decoration: underline; + text-decoration: underline; } caption { -font-size: 1.5em; -margin-bottom: 15px; -font-weight: 700; + font-size: 1.5em; + margin-bottom: 15px; + font-weight: 700; } diff --git a/public/css/tools.css b/public/css/tools.css index 5450e97..6781c3d 100644 --- a/public/css/tools.css +++ b/public/css/tools.css @@ -24,23 +24,6 @@ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); border-radius: 12px 12px 0 0; } -/* -.page-content h1 { - font-size: 3.2rem; - color: #1a202c; - margin-bottom: 1rem; - line-height: 1.2; - text-align: center; - font-weight: 800; - letter-spacing: -0.03em; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - padding-bottom: 1.5rem; - border-bottom: 3px solid #e2e8f0; - margin-bottom: 2.5rem; -} */ /* --- Section Headings (h1) --- */ .page-content h1 { @@ -92,7 +75,7 @@ margin-bottom: 2rem; } -/* Apply box styling ONLY to direct children LI of ul (top-level list items) */ +/* Apply box styling ONLY to direct children LI of ul */ .page-content ul > li { margin-bottom: 1.5rem; padding: 1.5rem 1.5rem 1.5rem 3rem; /* Spacing inside the box */ @@ -125,13 +108,13 @@ } /* --- Nested List Styling --- */ -/* Targets any ul that is inside another ul (e.g., the System Concepts list) */ +/* Targets any ul that is inside another ul */ .page-content ul ul { list-style: disc; /* Use default disc bullets for nested lists */ margin-top: 1rem; /* Space above the nested list */ margin-bottom: 0.5rem; margin-left: 2rem; /* Indent the nested list */ - padding-left: 0; /* Keep custom bullet formatting easier if you change list-style */ + padding-left: 0; } /* Styles for items within nested lists */ @@ -144,7 +127,7 @@ font-size: 1rem; /* Slightly smaller font for nested items */ color: #2d3748; /* Adjust color for nested items */ line-height: 1.5; /* Adjust line height for nested items */ - position: static; /* Remove absolute positioning if not needed for custom bullets */ + position: static; } /* Remove ::before for nested list items if default bullets are used */ @@ -153,7 +136,7 @@ } -/* --- Horizontal Rule (Separator) --- */ +/* --- Horizontal Rule --- */ .page-content hr { border: none; height: 1px; @@ -175,7 +158,7 @@ font-size: 1.2rem; } -/* --- Strong (Bold) Text --- */ +/* --- Strong Text --- */ .page-content strong { font-weight: 700; color: #4a5568; /* Ensures bold text is readable and consistent */ @@ -210,10 +193,6 @@ width: 100%; } -/* --- Social Media Links Special Styling (Buttons) --- */ -/* This rule needs to be applied carefully. It's currently styling ALL tags - inside ul li, which might include links within your tool descriptions. - If these are only for specific social media links, consider a more specific class. */ .page-content ul li a { display: inline-flex; align-items: center; @@ -245,11 +224,7 @@ margin: 0 1rem; border-radius: 8px; } -/* - .page-content h1 { - font-size: 2.5rem; - } */ - + .page-content h1 { font-size: 1.6rem; margin-top: 2.5rem; @@ -262,12 +237,6 @@ margin: 0 0.5rem; border-radius: 6px; } -/* - .page-content h1 { - font-size: 2rem; - padding-bottom: 1rem; - margin-bottom: 1.5rem; - } */ .page-content h1 { font-size: 1.4rem; @@ -306,16 +275,12 @@ .page-content { padding: 1rem 0.8rem; } -/* - .page-content h1 { - font-size: 1.8rem; - } */ .page-content h1 { font-size: 1.3rem; } - .page-content ul li a { /* This special link styling might need a class */ + .page-content ul li a { display: block; margin-bottom: 0.5rem; text-align: center; diff --git a/scripts/build-static.js b/scripts/build-static.js index 75f6d28..ed01c76 100644 --- a/scripts/build-static.js +++ b/scripts/build-static.js @@ -5,7 +5,7 @@ const { marked } = require("marked"); const handlebars = require("handlebars"); -const postsDir = path.join(__dirname, "posts"); +const postsDir = path.join(__dirname, "static", "posts"); const outputDir = path.join(__dirname, "static"); // Load and compile templates diff --git a/src/css/header.css b/src/css/header.css index a6ec8b0..24c33a7 100644 --- a/src/css/header.css +++ b/src/css/header.css @@ -7,13 +7,6 @@ z-index: 10; } -/* header#site-header .container { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - padding: 1rem 0; -} */ header#site-header .container { max-width: 800px; margin: 0 auto; diff --git a/src/css/toc.css b/src/css/toc.css index 5c5c5e1..fb04a87 100644 --- a/src/css/toc.css +++ b/src/css/toc.css @@ -1,4 +1,3 @@ -/* Enhanced TOC Styles */ .toc { background: #f8f9fa; border: 1px solid #e9ecef; @@ -26,7 +25,7 @@ overflow-y: auto; } -/* Custom Scrollbar - Subtle and Non-intrusive */ +/* Scrollbar */ .toc::-webkit-scrollbar { width: 6px; } diff --git a/src/services/sitemapService.js b/src/services/sitemapService.js index ac1f4eb..ad9f593 100644 --- a/src/services/sitemapService.js +++ b/src/services/sitemapService.js @@ -11,6 +11,8 @@ const { winstonLogger } = require("../utils/logging"); const CONTENT_ROOT = path.resolve(__dirname, "../../content"); +const NAVLINKS_PATH = path.resolve(__dirname, "../../content/navLinks.json"); + const pattern = `${CONTENT_ROOT}/**/*.md`; function slugifyTag(tag) { @@ -165,6 +167,77 @@ return entries; } + async getNavLinksPages(existingUrls = null) { + try { + const raw = await fs.readFile(NAVLINKS_PATH, "utf8"); + const navLinks = JSON.parse(raw); + + // If existingUrls not provided, get them (fallback for backward compatibility) + let existingSet; + if (existingUrls) { + existingSet = new Set( + existingUrls.map((url) => url.replace(/^https?:\/\/[^\/]+/, "")) + ); + } else { + const existing = await this.getAllUrls({ excludeNavLinks: true }); + existingSet = new Set( + existing.map((e) => e.loc.replace(/^https?:\/\/[^\/]+/, "")) + ); + } + + const transform = (items) => { + const result = []; + + for (const item of items) { + // Skip external links (not starting with "/") + const isInternal = item.href && item.href.startsWith("/"); + const isMissing = isInternal && !existingSet.has(item.href); + + const node = {}; + + // If this item has a missing internal href, add it as a leaf + if (isMissing) { + node.loc = item.href; + node.title = item.label || ""; + node.changefreq = "monthly"; + node.priority = 0.6; + node.id = hash(item.href); + } + + // Process submenu if it exists + if (item.submenu && Array.isArray(item.submenu)) { + const children = transform(item.submenu); + if (children.length > 0) { + // If we have children, we need to include this node in the hierarchy + if (!node.loc) { + // This is a parent node with no direct URL but has children + node.label = item.label || ""; + } + node.children = children; + } + } + + // Only include nodes that either have a loc (missing page) or have children + if (node.loc || (node.children && node.children.length > 0)) { + result.push(node); + } + } + + return result; + }; + + const extraPages = transform(navLinks); + + winstonLogger.debug( + `Generated ${extraPages.length} extra nav link entries` + ); + return extraPages; + } catch (err) { + winstonLogger.warn("Failed to read navLinks.json:", err); + return []; + } + } + injectPlaceholder(tree, key, items) { for (const node of tree) { if (Array.isArray(node.children)) { @@ -198,8 +271,7 @@ return []; } } - - async getCompleteSitemap() { + async getCompleteSitemap({ excludeNavLinks = false } = {}) { const [staticPagesJsonTree, staticPages, blogPosts, tags] = await Promise.all([ this._loadStaticLayout(), @@ -217,6 +289,7 @@ priority: page.priority, tags: page.tags, })); + const postItems = blogPosts.map((post) => ({ id: post.id, loc: post.loc, @@ -226,24 +299,39 @@ priority: post.priority, tags: post.tags, })); + const tagItems = tags.map((tag) => ({ title: tag.name, loc: tag.loc, slug: tag.slug, count: tag.count, })); + const docsEntries = await this.getDocsEntries(); + // Inject all the main content first this.injectPlaceholder(staticPagesJsonTree, "pages", pageItems); this.injectPlaceholder(staticPagesJsonTree, "blog-posts", postItems); this.injectPlaceholder(staticPagesJsonTree, "tags", tagItems); this.injectPlaceholder(staticPagesJsonTree, "docs", docsEntries); + // Now get all existing URLs from the current state + let extraNavPages = []; + if (!excludeNavLinks) { + const currentUrls = this.flatten(staticPagesJsonTree).map( + (item) => item.loc + ); + extraNavPages = await this.getNavLinksPages(currentUrls); + } + + // Inject the extra nav pages last + this.injectPlaceholder(staticPagesJsonTree, "extra-pages", extraNavPages); + return qualifySitemapLinks(staticPagesJsonTree); } - async getAllUrls() { - const sitemap = await this.getCompleteSitemap(); + async getAllUrls({ excludeNavLinks = false } = {}) { + const sitemap = await this.getCompleteSitemap({ excludeNavLinks }); return this.flatten(sitemap); }