diff --git a/assets/css/custom.scss b/assets/css/custom.scss
index 8696fb3..6058632 100644
--- a/assets/css/custom.scss
+++ b/assets/css/custom.scss
@@ -3524,6 +3524,78 @@ footer {
}
}
+// Blog sticky header with "Copy for LLM" button
+#blogStickyHeader {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ z-index: 100;
+ background: $pine;
+ padding: 0.55rem 1.45rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ transform: translateY(-100%);
+ transition: transform 0.3s ease;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+
+ &.visible {
+ transform: translateY(0);
+ }
+
+ .sticky-title {
+ color: #fff;
+ font-family: $header-font;
+ font-weight: 700;
+ font-size: 0.95rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: calc(100% - 200px);
+ line-height: 1.3;
+ }
+
+ #copyForLlm {
+ white-space: nowrap;
+ font-size: 0.75rem;
+ flex-shrink: 0;
+ transition: background 0.2s ease;
+
+ .fa {
+ margin-right: 0.25em;
+ }
+
+ &.copied {
+ background: #2ecc71;
+ }
+ }
+
+ @media (max-width: 767px) {
+ padding: 0.5rem 1rem;
+
+ .sticky-title {
+ font-size: 0.8rem;
+ max-width: calc(100% - 160px);
+ }
+
+ #copyForLlm {
+ font-size: 0.7rem;
+ }
+ }
+
+ @media (max-width: 575px) {
+ .sticky-title {
+ display: none;
+ }
+
+ #copyForLlm {
+ width: 100%;
+ text-align: center;
+ }
+ }
+}
+
// Header anchor links for blog posts (H1-H6)
// Entire heading is clickable with # symbol on the right and underline animation
.heading-with-anchor {
diff --git a/assets/js/copy-for-llm.js b/assets/js/copy-for-llm.js
new file mode 100644
index 0000000..fff4967
--- /dev/null
+++ b/assets/js/copy-for-llm.js
@@ -0,0 +1,141 @@
+/**
+ * Copy for LLM - Sticky Header with Copy-to-Clipboard
+ * Shows a sticky bar when scrolling past the main header on blog posts.
+ * Copies the blog content as clean markdown to the clipboard.
+ */
+(function () {
+ "use strict";
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init);
+ } else {
+ init();
+ }
+
+ function init() {
+ var stickyHeader = document.getElementById("blogStickyHeader");
+ var copyBtn = document.getElementById("copyForLlm");
+ var rawContent = document.getElementById("rawMarkdownContent");
+
+ if (!stickyHeader || !copyBtn || !rawContent) return;
+
+ // --- Scroll-based visibility ---
+ var pageHeader = document.querySelector("header[aria-label='header']");
+ if (!pageHeader) return;
+
+ function updateVisibility() {
+ var headerBottom = pageHeader.getBoundingClientRect().bottom;
+ if (headerBottom <= 0) {
+ stickyHeader.classList.add("visible");
+ } else {
+ stickyHeader.classList.remove("visible");
+ }
+ }
+
+ updateVisibility();
+
+ var scrollTimeout;
+ window.addEventListener(
+ "scroll",
+ function () {
+ if (!scrollTimeout) {
+ scrollTimeout = setTimeout(function () {
+ updateVisibility();
+ scrollTimeout = null;
+ }, 50);
+ }
+ },
+ { passive: true },
+ );
+
+ // --- Copy logic ---
+ copyBtn.addEventListener("click", function () {
+ var content = rawContent.textContent;
+ content = cleanContent(content);
+
+ copyToClipboard(content).then(function () {
+ showCopiedFeedback();
+ });
+ });
+
+ function cleanContent(text) {
+ // Fix relative or protocol-relative URL line — make it absolute
+ text = text.replace(
+ /^(URL: )(\/\/[^\s]*|\/[^\s]*)/m,
+ function (match, prefix, path) {
+ if (path.startsWith("//")) {
+ return prefix + window.location.protocol + path;
+ }
+ return prefix + window.location.origin + path;
+ },
+ );
+
+ // Convert lightboximg shortcodes to markdown images
+ // {{< lightboximg "/path/to/img.png" "Alt text" >}}
+ text = text.replace(
+ /\{\{<\s*lightboximg\s+"([^"]+)"\s+"([^"]+)"\s*>\}\}/g,
+ "",
+ );
+
+ // Remove signup shortcodes
+ text = text.replace(/\{\{<\s*signup\s*>\}\}/g, "");
+
+ // Remove other common shortcodes that don't translate to markdown
+ text = text.replace(
+ /\{\{<\s*(button|buttonout)\s+link="([^"]+)"\s+text="([^"]+)"\s*>\}\}/g,
+ "[$3]($2)",
+ );
+
+ // Convert HTML line breaks to newlines
+ text = text.replace(/
/gi, "\n");
+
+ // Clean up excessive blank lines (3+ newlines → 2)
+ text = text.replace(/\n{3,}/g, "\n\n");
+
+ // Trim leading/trailing whitespace
+ text = text.trim();
+
+ return text;
+ }
+
+ function copyToClipboard(text) {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ return navigator.clipboard.writeText(text).catch(function () {
+ return fallbackCopy(text);
+ });
+ }
+ return fallbackCopy(text);
+ }
+
+ function fallbackCopy(text) {
+ return new Promise(function (resolve, reject) {
+ var textarea = document.createElement("textarea");
+ textarea.value = text;
+ textarea.style.position = "fixed";
+ textarea.style.left = "-9999px";
+ textarea.style.top = "-9999px";
+ document.body.appendChild(textarea);
+ textarea.select();
+ try {
+ document.execCommand("copy");
+ resolve();
+ } catch (err) {
+ reject(err);
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ });
+ }
+
+ function showCopiedFeedback() {
+ var originalHTML = copyBtn.innerHTML;
+ copyBtn.innerHTML = ' Copied!';
+ copyBtn.classList.add("copied");
+
+ setTimeout(function () {
+ copyBtn.innerHTML = originalHTML;
+ copyBtn.classList.remove("copied");
+ }, 2000);
+ }
+ }
+})();
diff --git a/layouts/_default/single.html b/layouts/_default/single.html
index f95c347..cc86a03 100644
--- a/layouts/_default/single.html
+++ b/layouts/_default/single.html
@@ -2,6 +2,10 @@