diff --git a/assets/content/comment-pages.json b/assets/content/comment-pages.json new file mode 100644 index 0000000..3ddd9a4 --- /dev/null +++ b/assets/content/comment-pages.json @@ -0,0 +1,452 @@ +[ + { + "slug": "benefits-of-reading", + "title": "Benefits of Reading", + "url": "/blogs/2025/08-august/benefits-of-reading.html" + }, + { + "slug": "hilberts-hotel", + "title": "Hilbert's Hotel", + "url": "/blogs/2025/08-august/hilberts.hotel.html" + }, + { + "slug": "spending-all-day-on-this-website", + "title": "09-08-2025: Website Changes", + "url": "/blogs/2025/08-august/spending-the-whole-day-on-this-website.html" + }, + { + "slug": "third-time", + "title": "Third Time", + "url": "/blogs/2025/08-august/third-time.html" + }, + { + "slug": "wacom-with-arch", + "title": "Wacom With Arch", + "url": "/blogs/2025/08-august/wacom-with-arch.html" + }, + { + "slug": "what-i-want-to-do-with-emacs", + "title": "What Do I Want To Do With Emacs", + "url": "/blogs/2025/08-august/what-do-i-want-to-do-with-emacs.html" + }, + { + "slug": "zettlekasten", + "title": "Zettelkasten Method", + "url": "/blogs/2025/08-august/zettelkasten.html" + }, + { + "slug": "02-11-25-week-review", + "title": "[02-11-2025] - Weekly Review", + "url": "/blogs/2025/11-november/02-11-week-review.html" + }, + { + "slug": "09-11-25-week-review", + "title": "[09-11-2025] - Weekly Review", + "url": "/blogs/2025/11-november/09-11-week-review.html" + }, + { + "slug": "16-11-25-week-review", + "title": "[16-11-2025] - Weekly Review", + "url": "/blogs/2025/11-november/16-11-week-review.html" + }, + { + "slug": "23-11-25-week-review", + "title": "[23-11-2025] - Weekly Review", + "url": "/blogs/2025/11-november/23-11-week-review.html" + }, + { + "slug": "30-11-25-week-review", + "title": "[30-11-2025] - Weekly Review", + "url": "/blogs/2025/11-november/30-11-week-review.html" + }, + { + "slug": "07-12-25-week-review", + "title": "[07-12-2025] - Weekly Review", + "url": "/blogs/2025/12-december/07-12-week-review.html" + }, + { + "slug": "14-12-25-week-review", + "title": "[14-12-2025] - Weekly Review", + "url": "/blogs/2025/12-december/14-12-week-review.html" + }, + { + "slug": "21-12-25-week-review", + "title": "[21-12-2025] - Weekly Review", + "url": "/blogs/2025/12-december/21-12-week-review.html" + }, + { + "slug": "28-12-25-week-review", + "title": "[28-12-2025] - Weekly Review", + "url": "/blogs/2025/12-december/28-12-week-review.html" + }, + { + "slug": "04-01-26-week-review", + "title": "[04-01-2026] - Weekly Review", + "url": "/blogs/2026/01-january/04-01-week-review.html" + }, + { + "slug": "11-01-26-week-review", + "title": "[11-01-2026] - Weekly Review", + "url": "/blogs/2026/01-january/11-01-week-review.html" + }, + { + "slug": "18-01-26-week-review", + "title": "[18-01-2026] - Weekly Review", + "url": "/blogs/2026/01-january/18-01-week-review.html" + }, + { + "slug": "25-01-26-week-review", + "title": "[25-01-2026] - Weekly Review", + "url": "/blogs/2026/01-january/25-01-week-review.html" + }, + { + "slug": "01-02-26-week-review", + "title": "[01-02-2026] - Weekly Review", + "url": "/blogs/2026/02-february/01-02-week-review.html" + }, + { + "slug": "08-02-26-week-review", + "title": "[08-02-2026] - Weekly Review", + "url": "/blogs/2026/02-february/08-02-week-review.html" + }, + { + "slug": "15-02-26-week-review", + "title": "[15-02-2026] - Weekly Review", + "url": "/blogs/2026/02-february/15-02-week-review.html" + }, + { + "slug": "22-02-26-week-review", + "title": "[22-02-2026] - Weekly Review", + "url": "/blogs/2026/02-february/22-02-week-review.html" + }, + { + "slug": "24-02-26", + "title": "Integration tests failing (sob)", + "url": "/blogs/2026/02-february/24-02-26.html" + }, + { + "slug": "26-02-26", + "title": "Starting the Journeys Upgrade", + "url": "/blogs/2026/02-february/26-02-26.html" + }, + { + "slug": "27-02-26", + "title": "Journeys rambles again...", + "url": "/blogs/2026/02-february/27-02-26.html" + }, + { + "slug": "third-meeting-with-lima", + "title": "Third Meeting with lima :)", + "url": "/blogs/2026/02-february/third-meeting.html" + }, + { + "slug": "01-03-26-week-review", + "title": "[01-03-2026] - Weekly Review", + "url": "/blogs/2026/03-march/01-03-week-review.html" + }, + { + "slug": "08-03-26-week-review", + "title": "[08-03-2026] - Weekly Review", + "url": "/blogs/2026/03-march/08-03-week-review.html" + }, + { + "slug": "15-03-26-week-review", + "title": "[15-03-2026] - Weekly Review", + "url": "/blogs/2026/03-march/15-03-week-review.html" + }, + { + "slug": "22-03-26-week-review", + "title": "[22-03-2026] - Weekly Review", + "url": "/blogs/2026/03-march/22-03-week-review.html" + }, + { + "slug": "29-03-26-week-review", + "title": "[29-03-2026] - Weekly Review", + "url": "/blogs/2026/03-march/29-03-week-review.html" + }, + { + "slug": "cooking-dinner-29-03-26", + "title": "Cooking Dinner (num)", + "url": "/blogs/2026/03-march/cooking-dinner-29-03-26.html" + }, + { + "slug": "feeling-sleepy", + "title": "Feeling extremely sleepy", + "url": "/blogs/2026/03-march/feeling-sleepy.html" + }, + { + "slug": "fixing-the-dag-18-03", + "title": "DAG fixes", + "url": "/blogs/2026/03-march/fixing-the-dag-18-03.html" + }, + { + "slug": "installing-emacs-23-03", + "title": "Installing Emacs on Windows", + "url": "/blogs/2026/03-march/installing-emacs-23-03.html" + }, + { + "slug": "intellectually-challenging-myself-30-03-26", + "title": "Intellectually challenging oneself", + "url": "/blogs/2026/03-march/intellectually-challenging-myself-30-03-26.html" + }, + { + "slug": "more-thoughts-regarding-changes-26-03-26", + "title": "More thoughts on schedule changes", + "url": "/blogs/2026/03-march/more-thoughts-regarding-changes-26-03-26.html" + }, + { + "slug": "oversleeping-16-03", + "title": "Oversleeping and missing a meeting...", + "url": "/blogs/2026/03-march/oversleeping-16-03.html" + }, + { + "slug": "quick-lunch-24-03", + "title": "Making Lunch QUiCk!!", + "url": "/blogs/2026/03-march/quick-lunch-24-03.html" + }, + { + "slug": "setting-a-wird-tracker-19-03", + "title": "Setting a Wird Tracker", + "url": "/blogs/2026/03-march/setting-a-wird-tracker-19-03.html" + }, + { + "slug": "thoughts-on-life-25-03-26", + "title": "Thoughts on life...", + "url": "/blogs/2026/03-march/thoughts-on-life-25-03-26.html" + }, + { + "slug": "training-on-friday-afternoon-27-03-26", + "title": "Training time on Friday afternoon", + "url": "/blogs/2026/03-march/training-on-friday-afternoon-27-03-26.html" + }, + { + "slug": "05-04-26-week-review", + "title": "[05-04-2026] - Weekly Review", + "url": "/blogs/2026/04-april/05-04-week-review.html" + }, + { + "slug": "12-04-26-week-review", + "title": "[12-04-2026] - Weekly Review", + "url": "/blogs/2026/04-april/12-04-week-review.html" + }, + { + "slug": "16-04-26", + "title": "Rambles", + "url": "/blogs/2026/04-april/16-04-26.html" + }, + { + "slug": "19-04-26-week-review", + "title": "[19-04-2026] - Weekly Review", + "url": "/blogs/2026/04-april/19-04-week-review.html" + }, + { + "slug": "26-04-26-week-review", + "title": "[26-04-2026] - Weekly Review", + "url": "/blogs/2026/04-april/26-04-week-review.html" + }, + { + "slug": "action-plan-07-04-26", + "title": "Action plan to change teams", + "url": "/blogs/2026/04-april/action-plan-07-04-26.html" + }, + { + "slug": "adding-more-wird-22-04-26", + "title": "Adding another Wird", + "url": "/blogs/2026/04-april/adding-more-wird-22-04-26.html" + }, + { + "slug": "april-almost-over-28-04", + "title": "April is almost over...", + "url": "/blogs/2026/04-april/april-almost-over-28-04.html" + }, + { + "slug": "comparison-14-04", + "title": "Comparison is the thief of joy", + "url": "/blogs/2026/04-april/comparison-14-04.html" + }, + { + "slug": "ending-the-week-17-04-26", + "title": "End of week thoughts", + "url": "/blogs/2026/04-april/ending-the-week-17-04-26.html" + }, + { + "slug": "joining-new-team-meeting-02-04-2026", + "title": "New team meeting", + "url": "/blogs/2026/04-april/joining-new-team-meeting-02-04-2026.html" + }, + { + "slug": "making-notes-through-org-noter-06-05", + "title": "Making Notes through Org Noter", + "url": "/blogs/2026/04-april/making-notes-through-org-noter-06-05.html" + }, + { + "slug": "sitting-outside-in-the-sun-21-04-26", + "title": "Sitting in the sun", + "url": "/blogs/2026/04-april/sitting-outside-in-the-sun-21-04-26.html" + }, + { + "slug": "starting-new-rotation-08-04-26", + "title": "Starting new rotation", + "url": "/blogs/2026/04-april/starting-new-rotation-08-04-26.html" + }, + { + "slug": "weekly-target-06-04-26", + "title": "[06-04-2026] - Weekly target", + "url": "/blogs/2026/04-april/weekly-target-06-04-26.html" + }, + { + "slug": "wise-words-29-04", + "title": "Wise Words I need to engrain", + "url": "/blogs/2026/04-april/wise-words-29-04.html" + }, + { + "slug": "03-05-26-week-review", + "title": "[03-05-2026] - Weekly Review", + "url": "/blogs/2026/05-may/03-05-week-review.html" + }, + { + "slug": "ai-datacamp-08-05", + "title": "AI Datacamp", + "url": "/blogs/2026/05-may/ai-datacamp-08-05.html" + }, + { + "slug": "authoring-service-09-05-26", + "title": "Authoring Service", + "url": "/blogs/2026/05-may/authoring-service-09-05-26.html" + }, + { + "slug": "sun-soak-01-05", + "title": "Soaking in the sun", + "url": "/blogs/2026/05-may/sun-soak-01-05.html" + }, + { + "slug": "using-codex-07-05-26", + "title": "Using Codex", + "url": "/blogs/2026/05-may/using-codex-07-05-26.html" + }, + { + "slug": "publish-pages", + "title": "How to publish pages using Org Publish", + "url": "/blogs/publish-pages.html" + }, + { + "slug": "setup", + "title": "Setup", + "url": "/home/guide/setup.html" + }, + { + "slug": "backlog", + "title": "Backlog", + "url": "/home/backlog.html" + }, + { + "slug": "wedding-countdown", + "title": "Countdown", + "url": "/home/countdown.html" + }, + { + "slug": "notes", + "title": "Notes", + "url": "/home/notes.html" + }, + { + "slug": "services", + "title": "Service", + "url": "/home/services.html" + }, + { + "slug": "competency-board", + "title": "Competency Status Board", + "url": "/home/status.html" + }, + { + "slug": "airflow", + "title": "Datamarts, Airflow and DAG's", + "url": "/posts/career/airflow.html" + }, + { + "slug": "career-intro", + "title": "Career Introduction", + "url": "/posts/career/career-intro.html" + }, + { + "slug": "database-permissions", + "title": "Database Permissions, Roles, and Accounts", + "url": "/posts/career/database-permissions.html" + }, + { + "slug": "high-availability-disaster-recovery", + "title": "High Availability, Disaster Recovery and Business Continuity", + "url": "/posts/career/ha-dr.html" + }, + { + "slug": "invest-principles", + "title": "Invest Principles", + "url": "/posts/career/invest-principles.html" + }, + { + "slug": "understands-the-javascript-language", + "title": "Understands the Javascript language", + "url": "/posts/career/javascript.html" + }, + { + "slug": "lean", + "title": "Lean", + "url": "/posts/career/lean.html" + }, + { + "slug": "management-of-self", + "title": "Management of self training", + "url": "/posts/career/management-of-self.html" + }, + { + "slug": "monitoring-and-logging", + "title": "Monitoring and Logging", + "url": "/posts/career/monitoring-and-logging.html" + }, + { + "slug": "normalisation", + "title": "Benefits of Normalisation", + "url": "/posts/career/normalisation.html" + }, + { + "slug": "owasp-top-10", + "title": "OWASP Top Ten", + "url": "/posts/career/owasp.html" + }, + { + "slug": "pipelines-learning", + "title": "Pipelines and how they work (as well as CI/CD)", + "url": "/posts/career/pipelines.html" + }, + { + "slug": "probation-objectives", + "title": "Probation Objectives:", + "url": "/posts/career/probation-objectives.html" + }, + { + "slug": "requirements-features", + "title": "Requirements, features, user stories, tasks, walking skeletons", + "url": "/posts/career/requirements-features.html" + }, + { + "slug": "restful-api", + "title": "Restful API", + "url": "/posts/career/restful-api.html" + }, + { + "slug": "retrospectives", + "title": "Retrospectives", + "url": "/posts/career/retrospectives.html" + }, + { + "slug": "solid-principles", + "title": "SOLID Principles", + "url": "/posts/career/solid-principles.html" + }, + { + "slug": "wireframe-designs", + "title": "Wireframe Designs", + "url": "/posts/career/wireframe-designs.html" + } +] diff --git a/assets/scripts/zhd.js b/assets/scripts/zhd.js index 23378dd..83f83d2 100755 --- a/assets/scripts/zhd.js +++ b/assets/scripts/zhd.js @@ -69,6 +69,10 @@ .replace(/"/g, """); } + function escAttr(str) { + return escHtml(str).replace(/'/g, "'"); + } + function normaliseHref(href, prefix) { if (!href || href.startsWith("http") || href.startsWith("#")) return null; if (href.startsWith("/")) return href; @@ -132,6 +136,129 @@ } } + function commentSlug(comment) { + return comment.pageSlug || comment.page_slug || ""; + } + + function commentDate(comment) { + return comment.created_at || comment.createdAt || ""; + } + + function formatCommentDate(value) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return ""; + + return new Intl.DateTimeFormat(undefined, { + month: "short", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }).format(date); + } + + function commentExcerpt(content) { + const text = String(content || "").replace(/\s+/g, " ").trim(); + if (text.length <= 150) return text; + return text.slice(0, 147).trimEnd() + "..."; + } + + function renderRecentComments(comments, pageMap) { + const ul = document.getElementById("db-feed-comments"); + if (!ul) return; + + if (!comments.length) { + ul.innerHTML = ( + `
  • ` + + `` + + `
    ` + + `No comments yet.` + + `
    ` + + `
  • ` + ); + return; + } + + ul.innerHTML = comments.map((comment) => { + const slug = commentSlug(comment); + const page = pageMap.get(slug); + const title = page?.title || slug || "Unknown page"; + const href = page?.url ? page.url + "#comments" : null; + const author = comment.author || "Anonymous"; + const date = formatCommentDate(commentDate(comment)); + const pageLink = href + ? `${escHtml(title)}` + : `${escHtml(title)}`; + + return ( + `
  • ` + + `` + + `
    ` + + `
    ` + + `${escHtml(author)}` + + `on` + + pageLink + + (date ? `` : "") + + `
    ` + + `

    ${escHtml(commentExcerpt(comment.content))}

    ` + + `
    ` + + `
  • ` + ); + }).join(""); + } + + async function loadCommentPageMap() { + try { + const resp = await fetch("/assets/content/comment-pages.json", { credentials: "same-origin" }); + if (!resp.ok) return new Map(); + + const pages = await resp.json(); + return new Map( + pages + .filter((page) => page.slug && page.url) + .map((page) => [page.slug, page]) + ); + } catch (_) { + return new Map(); + } + } + + async function loadRecentComments() { + const ul = document.getElementById("db-feed-comments"); + if (!ul) return; + + const renderUnavailable = () => { + ul.innerHTML = ( + `
  • ` + + `` + + `
    ` + + `Recent comments are unavailable.` + + `
    ` + + `
  • ` + ); + }; + + try { + const [pageMap, resp] = await Promise.all([ + loadCommentPageMap(), + fetch("/api/comments", { credentials: "same-origin" }), + ]); + if (!resp.ok) { + renderUnavailable(); + return; + } + + const comments = await resp.json(); + const recent = comments + .filter((comment) => commentDate(comment)) + .sort((a, b) => new Date(commentDate(b)) - new Date(commentDate(a))) + .slice(0, 10); + + renderRecentComments(recent, pageMap); + } catch (_) { + renderUnavailable(); + } + } + function initCommandFilter() { const input = document.getElementById("db-command-search"); const nav = document.getElementById("db-quicknav"); @@ -169,6 +296,7 @@ loadFeed("/blogs/blogs-list.html", "db-feed-blogs", 6, "/blogs"); loadFeed("/posts/posts-list.html", "db-feed-posts", 6, "/posts"); loadFeed("/recently-updated.html", "db-feed-recent", 6, ""); + loadRecentComments(); } if (document.readyState === "loading") { diff --git a/assets/styles/zhd.css b/assets/styles/zhd.css index 2295249..61d74ae 100755 --- a/assets/styles/zhd.css +++ b/assets/styles/zhd.css @@ -351,6 +351,81 @@ white-space: nowrap; } +.db-comment-feed { + display: grid; + gap: 0; + list-style: none; + margin: 0; + padding: 0; +} + +.db-comment-feed__item { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 0.65rem; + align-items: start; + min-width: 0; + padding: 0.72rem 0; + margin: 0; + border-top: 1px solid color-mix(in oklab, var(--db-line) 70%, transparent); +} + +.db-comment-feed__item::before { + content: none !important; +} + +.db-comment-feed__body { + min-width: 0; +} + +.db-comment-feed__top { + display: flex; + flex-wrap: wrap; + gap: 0.32rem; + align-items: baseline; + color: var(--muted); + font-size: 0.78rem; + line-height: 1.35; +} + +.db-comment-feed__top strong { + color: var(--fg); + font-size: 0.86rem; +} + +.db-comment-feed__page { + color: var(--link); + font-weight: 800; + text-decoration: none; +} + +a.db-comment-feed__page:hover { + text-decoration: underline; +} + +.db-comment-feed__top time { + margin-left: auto; + color: var(--muted); + font-size: 0.74rem; + font-variant-numeric: tabular-nums; + font-weight: 700; + white-space: nowrap; +} + +.db-comment-feed__body p { + margin: 0.22rem 0 0; + color: var(--db-text-soft); + font-size: 0.9rem; + line-height: 1.45; + overflow-wrap: anywhere; +} + +.db-comment-feed__empty { + color: var(--muted); + font-size: 0.9rem; + font-style: italic; +} + .db-stats-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); diff --git a/build-site.el b/build-site.el index d17788c..ed59f65 100755 --- a/build-site.el +++ b/build-site.el @@ -145,6 +145,7 @@ FMT / ARGS are passed to `format'." (require 'ox-publish) (require 'cl-lib) +(require 'json) (require 'org) (require 'ox) (require 'ox-html) @@ -339,6 +340,47 @@ Created with %c on Arch \ slug (file-name-base file))))) +(defun z/comment-page-url (file) + "Return root-relative published HTML URL for source FILE." + (concat "/" + (file-name-sans-extension + (file-relative-name file z/site-root)) + ".html")) + +(defun z/comment-page-title (file) + "Return exported page title for FILE, or the file basename." + (with-temp-buffer + (insert-file-contents file nil 0 4096) + (goto-char (point-min)) + (if (re-search-forward "^#\\+TITLE:[ \t]*\\(.+?\\)[ \t]*$" nil t) + (match-string 1) + (file-name-base file)))) + +(defun z/comment-page-record (file) + "Return a JSON-ready comment page record for FILE." + `((slug . ,(z/comments-file-slug file)) + (title . ,(z/comment-page-title file)) + (url . ,(z/comment-page-url file)))) + +(defun z/generate-comment-pages-json () + "Write a slug-to-page manifest for comment-enabled Org files." + (let* ((files (directory-files-recursively z/site-root "\\.org\\'")) + (records + (cl-loop for file in files + unless (or (string-prefix-p (site-path "output/") file) + (string-prefix-p (site-path "backups/") file) + (string-prefix-p (site-path ".org-timestamps/") file) + (string-prefix-p (site-path ".packages/") file)) + when (z/comments-file-p file) + collect (z/comment-page-record file))) + (target (site-path "assets/content/comment-pages.json")) + (json-encoding-pretty-print t)) + (make-directory (file-name-directory target) t) + (with-temp-file target + (insert (json-encode (vconcat records))) + (insert "\n")) + (z/log 'done "Wrote comment page manifest (%d pages)" (length records)))) + (defun z/org-html-insert-comments-into-body (body backend info) "Append a comments section to BODY for files that opt in." (when (org-export-derived-backend-p backend 'html) @@ -760,6 +802,13 @@ A nil result means the timestamp cache is stale and we must force-republish." (z/log 'error "Tag generation failed: %s" (error-message-string err)) (error "Aborting build: tag generation error")))) +(z/with-timing "comment page manifest" + (condition-case err + (z/generate-comment-pages-json) + (error + (z/log 'error "Comment page manifest failed: %s" (error-message-string err)) + (error "Aborting build: comment page manifest error")))) + ;; ───────────────────────────────────────────────────────────────────────────── ;; 16. Publish ;; ───────────────────────────────────────────────────────────────────────────── diff --git a/index.org b/index.org index c024a8b..26ffee9 100755 --- a/index.org +++ b/index.org @@ -170,6 +170,23 @@ +
    +
    +
    +

    Discussion

    +

    Recent comments

    +
    +
    + +
    +