<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Blog | Lin Shang</title><link>https://lin-shang.github.io/blog/</link><atom:link href="https://lin-shang.github.io/blog/index.xml" rel="self" type="application/rss+xml"/><description>Blog</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Wed, 17 Dec 2025 00:00:00 +0800</lastBuildDate><image><url>https://lin-shang.github.io/media/icon_hu_7fea8c3aa2e7dad1.png</url><title>Blog</title><link>https://lin-shang.github.io/blog/</link></image><item><title>如果不只是展示代码：Run Python in My Hugo blog.</title><link>https://lin-shang.github.io/blog/interactive_python_demo/</link><pubDate>Wed, 17 Dec 2025 00:00:00 +0800</pubDate><guid>https://lin-shang.github.io/blog/interactive_python_demo/</guid><description>&lt;h2 id="背景为什么我们需要活的代码"&gt;背景：为什么我们需要“活”的代码？&lt;/h2&gt;
&lt;p&gt;作为一名计算机科学/AI方向的研究者，我们在撰写技术博客时，常常需要展示算法逻辑或数据处理流程。传统的 Hugo 博客通过 Markdown 的代码块（Code Block）展示代码，虽然美观，但它是&lt;strong&gt;死&lt;/strong&gt;的。&lt;/p&gt;
&lt;p&gt;读者无法修改参数来验证想法，无法查看中间变量的状态，更无法直观地看到绘图结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果能在博客里直接嵌入一个类似 Jupyter Notebook 的可交互环境，岂不美哉？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;起初我认为这需要部署一个后端服务（如 JupyterHub），直到我发现了 &lt;strong&gt;PyScript&lt;/strong&gt; 和 &lt;strong&gt;WebAssembly (WASM)&lt;/strong&gt;。这意味着我们可以在&lt;strong&gt;纯静态&lt;/strong&gt;的网页（如 GitHub Pages）中，利用&lt;strong&gt;浏览器&lt;/strong&gt;的算力来运行 Python，无需任何服务器成本。&lt;/p&gt;
&lt;p&gt;本文将复盘我从原型到最终实现的完整踩坑历程，并提供一套开箱即用的解决方案。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="核心技术栈"&gt;核心技术栈&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hugo (Extended)&lt;/strong&gt;: 静态网站生成器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PyScript (Pyodide)&lt;/strong&gt;: 基于 WASM 的浏览器端 Python 运行时。&lt;/li&gt;
&lt;li&gt;CodeJar + PrismJS: 关键组件。CodeJar 处理编辑行为（如 Tab 缩进、光标管理），PrismJS 负责语法高亮。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Micropip&lt;/strong&gt;: 浏览器端的 Python 包管理器（用于安装 Numpy 等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="避坑指南由死到活的四个难点"&gt;避坑指南：由“死”到“活”的四个难点&lt;/h2&gt;
&lt;p&gt;在直接给出代码之前，我想先分享开发过程中遇到的几个“史诗级”大坑，这能帮你理解为什么代码要写成最终那个样子。&lt;/p&gt;
&lt;h3 id="1-浏览器的安全锁-coopcoep"&gt;1. 浏览器的“安全锁” (COOP/COEP)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：Python 环境根本无法启动，控制台报错 &lt;code&gt;SharedArrayBuffer is not defined&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：为了防止幽灵熔断（Spectre）攻击，现代浏览器默认禁用了高精度计时和共享内存，除非服务器发送特定的安全响应头。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：GitHub Pages 无法配置服务器头。我们必须使用 &lt;code&gt;coi-serviceworker.js&lt;/code&gt; 脚本，在前端通过 Service Worker 欺骗浏览器，开启隔离环境。&lt;/p&gt;
&lt;h3 id="2-html-压缩导致的缩进消失术"&gt;2. HTML 压缩导致的“缩进消失术”&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：本地运行正常，推送到 GitHub 后，Python 代码的所有缩进全没了，导致 &lt;code&gt;IndentationError&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：GitHub Actions 构建时使用了 &lt;code&gt;hugo --minify&lt;/code&gt;。HTML 压缩器认为 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; 里的空格是多余的，直接删除了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;strong&gt;Base64 传输法&lt;/strong&gt;。在 Hugo 模板层将代码转为 Base64 字符串，传给前端后再用 JS 解码。HTML 压缩器看不懂 Base64，自然不敢乱动。&lt;/p&gt;
&lt;h3 id="3-编辑体验contenteditable-vs-高亮"&gt;3. 编辑体验：contentEditable vs 高亮&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：普通的 textarea 没有高亮；使用 div contenteditable 虽然可以渲染 HTML 标签来高亮，但每次输入会导致光标乱跳。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：引入 CodeJar。这是一个仅 2KB 的微型库，它接管了 contenteditable 元素的光标和输入事件，完美配合 PrismJS 实现实时高亮。&lt;/p&gt;
&lt;h3 id="4-无法加载第三方库"&gt;4. 无法加载第三方库&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;import numpy&lt;/code&gt; 报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：浏览器里没有预装这些库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：在 PyScript 启动配置中预声明 &lt;code&gt;packages&lt;/code&gt;，并使用 &lt;code&gt;micropip&lt;/code&gt; 进行动态加载。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="手把手教程复刻同款-mini-ide"&gt;手把手教程：复刻同款 Mini IDE&lt;/h2&gt;
&lt;p&gt;请按照以下目录结构创建或修改你的 Hugo 项目文件。&lt;/p&gt;
&lt;h3 id="第一步配置-service-worker-static"&gt;第一步：配置 Service Worker (static)&lt;/h3&gt;
&lt;p&gt;创建文件 &lt;code&gt;static/js/coi-serviceworker.js&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下载地址：
&lt;em&gt;(将该文件内容完整复制进去即可)&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="第二步注入依赖库-head-hooks"&gt;第二步：注入依赖库 (Head Hooks)&lt;/h3&gt;
&lt;p&gt;我们需要在页面头部引入 PyScript、PrismJS（高亮）和 Service Worker。
创建或修改 &lt;code&gt;layouts/_partials/hooks/head-end/custom_style.html&lt;/code&gt;：&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Details&lt;/summary&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-txt" data-lang="txt"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;直接查看 https://github.com/LIN-SHANG/lin-shang.github.io/tree/main/layouts/_partials/hooks/head-end/custom_style.html
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/details&gt;
&lt;h3 id="第三步打造-shortcode-组件-核心"&gt;第三步：打造 Shortcode 组件 (核心)&lt;/h3&gt;
&lt;p&gt;这是集大成者。它实现了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Base64 传输：防止 Hugo 压缩破坏代码格式。&lt;/li&gt;
&lt;li&gt;CodeJar 集成：提供 IDE 般的编辑体验。&lt;/li&gt;
&lt;li&gt;变量监控树：右侧实时显示 Python 变量结构。&lt;/li&gt;
&lt;li&gt;Matplotlib 绘图与导出：支持画图并一键复制为 PNG。&lt;/li&gt;
&lt;li&gt;Ctrl+Enter 运行选中代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建文件 &lt;code&gt;layouts/shortcodes/py-ide.html&lt;/code&gt;
&lt;details&gt;
&lt;summary&gt;Details&lt;/summary&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-txt" data-lang="txt"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;直接查看 https://github.com/LIN-SHANG/lin-shang.github.io/tree/main/layouts/shortcodes/py-ide.html
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/details&gt;
&lt;/p&gt;
&lt;h3 id="第四步部署配置-deployyml"&gt;第四步：部署配置 (deploy.yml)&lt;/h3&gt;
&lt;p&gt;最后，也是最容易被忽视的一步。为了让 Hugo 正确编译上述代码，GitHub Actions 必须使用 &lt;strong&gt;Extended&lt;/strong&gt; 版本。&lt;/p&gt;
&lt;p&gt;修改 &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;前面的很多代码
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Setup Hugo&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;peaceiris/actions-hugo@v3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hugo-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;0.152.2&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 建议固定一个较新版本,不建议低于 0.148&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;extended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# &amp;lt;--- 必须开启！&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;后面的很多代码
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这是我生成的 PyTorch 执行过程演示：
&lt;div id="trace-wrapper-2" class="stanford-trace-wrapper" style="
position: relative; margin: 2rem 0; width: 100%; border-radius: 8px; overflow: hidden;
background: transparent; border: 1px solid var(--ide-border, #d0d7de);
/* 初始高度 */
min-height: 150px;
transition: border-color 0.3s;
"&gt;
&lt;div style="position: absolute; top: 9px; right: 12px; z-index: 50; display: flex; gap: 8px;"&gt;
&lt;button id="trace-fs-2" onclick="toggleFullscreen('2')" style="
background: transparent; border: 1px solid rgba(127,127,127,0.2); color: inherit;
padding: 3px 8px; border-radius: 4px; cursor: pointer;
font-family: -apple-system, sans-serif; font-size: 14px; line-height: 1;
backdrop-filter: blur(2px); transition: all 0.2s; opacity: 0.6;
" title="Toggle Fullscreen"&gt;⛶&lt;/button&gt;
&lt;button id="trace-copy-2" onclick="copyTrace('2')" style="
background: transparent; border: 1px solid rgba(127,127,127,0.2); color: inherit;
padding: 3px 8px; border-radius: 4px; cursor: pointer;
font-family: -apple-system, sans-serif; font-size: 11px; font-weight: 600;
backdrop-filter: blur(2px); transition: all 0.2s; opacity: 0.6;
"&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;iframe
id="trace-iframe-2"
src='https://lin-shang.github.io/cs336-viewer/?trace=%2ftraces%2ftrace_source_code.demo.json'
width="100%" height="200px" scrolling="no" frameborder="0"
style="display: block; width: 100%; border: none;" allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;style&gt;
.stanford-trace-wrapper:fullscreen { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; background: var(--bg-color, #fff); }
.stanford-trace-wrapper:fullscreen iframe { height: 95vh !important; width: 100% !important; max-width: 1600px; }
.stanford-trace-wrapper:-webkit-full-screen { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; }
.stanford-trace-wrapper:-webkit-full-screen iframe { height: 95vh !important; width: 100% !important; }
&lt;/style&gt;
&lt;script&gt;
window.toggleFullscreen = function(id) {
const wrapper = document.getElementById('trace-wrapper-' + id);
const btn = document.getElementById('trace-fs-' + id);
if (!document.fullscreenElement) {
if(wrapper.requestFullscreen) wrapper.requestFullscreen();
else if(wrapper.webkitRequestFullscreen) wrapper.webkitRequestFullscreen();
btn.innerHTML = "✖"; btn.style.borderColor = "#2da44e"; btn.style.color = "#2da44e"; btn.style.opacity = "1";
} else {
if(document.exitFullscreen) document.exitFullscreen();
else if(document.webkitExitFullscreen) document.webkitExitFullscreen();
btn.innerHTML = "⛶"; btn.style.borderColor = "rgba(127,127,127,0.2)"; btn.style.color = "inherit"; btn.style.opacity = "0.6";
}
};
window.copyTrace = function(id) {
const iframe = document.getElementById('trace-iframe-' + id);
if(!iframe || !iframe.contentDocument) return;
const doc = iframe.contentDocument;
const lineElements = doc.querySelectorAll('.line');
if (lineElements.length &gt; 0) {
const codeLines = Array.from(lineElements).map(lineEl =&gt; {
if (lineEl.children.length &gt;= 2) return lineEl.lastElementChild.innerText;
return lineEl.innerText.replace(/^\s*\d+/, '');
});
copyToClip(codeLines.join('\n'), id);
} else {
const raw = doc.body.innerText;
copyToClip(raw, id);
}
};
function copyToClip(text, id) {
navigator.clipboard.writeText(text).then(() =&gt; {
const btn = document.getElementById('trace-copy-' + id);
const originalText = btn.innerText;
btn.innerText = "✓"; btn.style.color = "#2da44e"; btn.style.borderColor = "#2da44e"; btn.style.opacity = "1";
setTimeout(() =&gt; { btn.innerText = originalText; btn.style.color = "inherit"; btn.style.borderColor = "rgba(127,127,127,0.2)"; btn.style.opacity = "0.6"; }, 2000);
});
}
(function() {
const iframe = document.getElementById("trace-iframe-2");
const wrapper = iframe.parentElement;
const isAutoHeight = "auto" === "auto";
let resizeObserver = null;
function updateTheme() {
if (!iframe.contentDocument) return;
const doc = iframe.contentDocument;
const html = document.documentElement;
const isDark = html.classList.contains('dark') || html.getAttribute('data-theme') === 'dark';
if (isDark) doc.documentElement.classList.add('dark');
else doc.documentElement.classList.remove('dark');
const globalFont = getComputedStyle(document.documentElement).getPropertyValue('--global-code-font') || "monospace";
let st = doc.getElementById('hugo-inj-layout');
if(!st) { st = doc.createElement('style'); st.id='hugo-inj-layout'; doc.head.appendChild(st); }
st.innerHTML = `
/* 强制 html/body 只有自动高度，不许继承 iframe 高度 */
html, body {
height: auto !important;
min-height: 0 !important;
overflow: hidden !important; /* 让 iframe 外部不可滚动，完全依赖 iframe 高度 */
margin: 0 !important;
padding: 0 !important;
}
#root {
height: auto !important;
min-height: 0 !important;
display: block !important; /* 确保它是块级，撑开内容 */
}
/* 确保内容容器不被拉伸 */
.trace-viewer-container {
height: auto !important;
min-height: 0 !important;
}
/* --- 仅新增：强制所有元素使用父页面的字体 --- */
* {
font-family: ${globalFont} !important;
}
`;
}
function initAutoResize() {
if (!isAutoHeight || !iframe.contentDocument) return;
const doc = iframe.contentDocument;
const target = doc.querySelector('.trace-viewer-container') || doc.getElementById('root') || doc.body;
if (resizeObserver) resizeObserver.disconnect();
resizeObserver = new ResizeObserver(entries =&gt; {
if (document.fullscreenElement) return;
for (let entry of entries) {
const rect = entry.target.getBoundingClientRect();
let newHeight = rect.height;
if (newHeight &lt; 50) newHeight = entry.target.scrollHeight;
newHeight += 20;
if (Math.abs(iframe.clientHeight - newHeight) &gt; 5) {
iframe.style.height = newHeight + "px";
}
}
});
resizeObserver.observe(target);
}
iframe.onload = () =&gt; {
updateTheme();
setTimeout(initAutoResize, 200);
};
new MutationObserver(updateTheme).observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] });
document.addEventListener('fullscreenchange', () =&gt; {
const btn = document.getElementById('trace-fs-2');
if (!document.fullscreenElement) {
btn.innerHTML = "⛶"; btn.style.borderColor = "rgba(127,127,127,0.2)"; btn.style.color = "inherit"; btn.style.opacity = "0.6";
setTimeout(initAutoResize, 200);
}
});
})();
&lt;/script&gt;&lt;/p&gt;
&lt;h2 id="效果展示与使用"&gt;效果展示与使用&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;功能亮点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;逐行运行&lt;/strong&gt;：选中某一行，按 Ctrl + Enter，仅运行选中部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;变量探查&lt;/strong&gt;：右侧会自动解析当前的变量，字典和列表可以折叠展开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绘图支持&lt;/strong&gt;：调用 show_plot() 即可显示 Matplotlib 图像。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态保持&lt;/strong&gt;：变量在多次运行之间是共享的，无需重复定义。&lt;/li&gt;
&lt;/ol&gt;
&lt;style&gt;
.py-ide-wrapper {
--ide-font: var(--global-code-font, monospace);
--ide-size: var(--global-code-size, 14.5px);
--ide-lh: var(--global-line-height, 1.6);
--ide-bg: #ffffff;
--ide-fg: #24292e;
--ide-border: #d0d7de;
--ide-header-bg: #f6f8fa;
--ide-btn-run: #2da44e;
--ide-btn-text: #ffffff;
--ide-shadow: 0 4px 12px rgba(0,0,0,0.08);
--tree-key: #005cc5;
--tree-val: #032f62;
--tree-type: #6a737d;
}
:root[data-theme="dark"] .py-ide-wrapper,
html.dark .py-ide-wrapper,
body.dark .py-ide-wrapper {
--ide-bg: #161b22;
--ide-fg: #d4d4d4;
--ide-border: #30363d;
--ide-header-bg: #0d1117;
--ide-btn-run: #238636;
--ide-btn-text: #ffffff;
--ide-shadow: 0 8px 24px rgba(0,0,0,0.5);
--tree-key: #79c0ff;
--tree-val: #a5d6ff;
--tree-type: #8b949e;
}
.py-ide-container {
background-color: var(--ide-bg);
color: var(--ide-fg);
border: 1px solid var(--ide-border);
border-radius: 8px;
margin: 2rem 0;
overflow: hidden;
display: flex; flex-wrap: wrap; width: 100%;
box-shadow: var(--ide-shadow);
transition: all 0.3s ease;
}
.py-header {
height: 40px; padding: 0 12px; display: flex; justify-content: space-between; align-items: center;
background-color: var(--ide-header-bg); border-bottom: 1px solid var(--ide-border);
font-family: -apple-system, sans-serif; font-size: 12px; font-weight: 600; color: var(--ide-fg);
}
.py-editor {
min-height: 200px; padding: 15px;
font-family: var(--ide-font) !important;
font-size: var(--ide-size) !important;
line-height: var(--ide-lh) !important;
background-color: transparent; color: inherit; outline: none; white-space: pre; overflow: auto;
}
.py-output {
padding: 10px 15px; border-top: 1px solid var(--ide-border);
background-color: rgba(127,127,127,0.05); color: var(--ide-fg);
font-family: var(--ide-font) !important;
font-size: 13px;
white-space: pre-wrap; max-height: 250px; overflow-y: auto;
}
.py-main-col { flex: 1; min-width: 300px; display: flex; flex-direction: column; border-right: 1px solid var(--ide-border); }
.py-sidebar-col { width: 260px; border-left: 1px solid var(--ide-border); display: flex; flex-direction: column; font-size: 13px; font-family: var(--ide-font); }
.py-btn-group { display: flex; gap: 8px; align-items: center; }
.py-run-btn {
background: var(--ide-btn-run) !important; color: var(--ide-btn-text) !important;
border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer;
font-weight: 600; font-size: 11px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);
transition: transform 0.1s, opacity 0.2s;
}
.py-run-btn:active { transform: translateY(1px); }
.py-icon-btn {
background: transparent; color: var(--ide-fg); border: 1px solid var(--ide-border);
padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 11px;
}
.py-tree-container { padding: 10px; flex: 1; overflow-y: auto; line-height: 1.5; }
.t-node { margin-left: 10px; }
.t-key { color: var(--tree-key); font-weight: bold; }
.t-val { color: var(--tree-val); }
.t-type { color: var(--tree-type); font-size: 0.85em; margin-left: 5px; font-style: italic; }
.t-arrow { display: inline-block; width: 12px; cursor: pointer; opacity: 0.7; font-size: 10px; user-select: none; }
.t-children { display: none; border-left: 1px solid var(--ide-border); margin-left: 5px; padding-left: 5px; }
.t-children.open { display: block; }
.py-plot-container { background: white; padding: 15px; text-align: center; border-top: 1px solid var(--ide-border); display: none; }
&lt;/style&gt;
&lt;div class="py-ide-wrapper"&gt;
&lt;div class="py-ide-container"&gt;
&lt;div class="py-main-col"&gt;
&lt;div class="py-header"&gt;
&lt;span&gt;TERMINAL&lt;/span&gt;
&lt;div class="py-btn-group"&gt;
&lt;button id="copy-code-3" class="py-icon-btn" onclick="copyIdeCode('3')"&gt;COPY&lt;/button&gt;
&lt;button class="py-icon-btn" py-click="reset_code_3"&gt;RESET&lt;/button&gt;
&lt;button id="btn-3" class="py-run-btn" py-click="run_code_3"&gt;▶ RUN&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id="editor-3" class="py-editor language-python" data-code="IyDwn5KhIOWwj&amp;#43;aKgOW3p&amp;#43;&amp;#43;8muWKqOaAgeWuieijheesrOS4ieaWueW6kyDwn5iOCiMg5Y&amp;#43;q6ZyA6KaBICMgaW5zdGFsbDogcGFja2FnZV8xLCBwYWNrYWdlXzIgCiMgaW5zdGFsbDogcGFuZGFzCmltcG9ydCBudW1weSBhcyBucCAKaW1wb3J0IG1hdHBsb3RsaWIucHlwbG90IGFzIHBsdCAKCiMgMS4g5a6a5LmJ5pWw5o2uCnggPSBucC5saW5zcGFjZSgwLCAxMCwgMTAwKSAKeSA9IG5wLnNpbih4KSAKeTIgPSBucC5jb3MoeCkKCiMgMi4g57uY5Yi25Zu&amp;#43;5YOPCnBsdC5maWd1cmUoZmlnc2l6ZT0oNiwgNCkpCnBsdC5wbG90KHgsIHksIGxhYmVsPSdTaW4nLCBjb2xvcj0nIzRDQUY1MCcpCnBsdC5wbG90KHgsIHkyLCBsYWJlbD0nQ29zJywgY29sb3I9JyNGRkMxMDcnLCBsaW5lc3R5bGU9Jy0tJykKcGx0LnRpdGxlKCJJbnRlcmFjdGl2ZSBQbG90IGluIEh1Z28iKSAKcGx0LmxlZ2VuZCgpCnBsdC5ncmlkKFRydWUsIGFscGhhPTAuMykKCiMgMy4g5pi&amp;#43;56S65Zu&amp;#43;5YOPCnNob3dfcGxvdCgpIAoKIyA0LiDov5nph4znmoTlj5jph48geCDlkowgeSDkvJroh6rliqjmmL7npLrlnKjlj7Pkvqflj5jph4/moJHkuK0="&gt;&lt;/div&gt;
&lt;div class="py-output" id="output-3"&gt;&gt; Ready.&lt;/div&gt;
&lt;div id="plot-header-3" class="py-header" style="display: none; justify-content: flex-end; border-top: 1px solid var(--ide-border);"&gt;
&lt;button id="copy-png-btn-3" class="py-icon-btn"&gt;Save PNG&lt;/button&gt;
&lt;/div&gt;
&lt;div class="py-plot-container" id="plot-3"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="py-sidebar-col"&gt;
&lt;div class="py-header"&gt;VARIABLES&lt;/div&gt;
&lt;div id="tree-3" class="py-tree-container"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;script type="module"&gt;
import { CodeJar } from 'https://cdn.jsdelivr.net/npm/codejar@3.7.0/codejar.min.js';
window.renderJsonTree = function(id, jsonStr) {
const container = document.getElementById(id);
if(!container) return;
let data = {};
try { data = JSON.parse(jsonStr); } catch(e) { return; }
if (Object.keys(data).length === 0) {
container.innerHTML = '&lt;div style="color:var(--tree-type); padding:5px;"&gt;(Empty)&lt;/div&gt;';
return;
}
const buildTree = (obj) =&gt; {
let html = '';
for (const [key, info] of Object.entries(obj)) {
const hasChild = info.children !== null;
const arrow = hasChild ? '▶' : ' ';
const val = info.val;
const type = info.type;
const childHtml = hasChild ? `&lt;div class="t-children"&gt;${buildTree(info.children)}&lt;/div&gt;` : '';
html += `
&lt;div class="t-node"&gt;
&lt;span class="t-arrow" onclick="toggleTree(this)"&gt;${arrow}&lt;/span&gt;
&lt;span class="t-key"&gt;${key}&lt;/span&gt;:
&lt;span class="t-val"&gt;${val}&lt;/span&gt;
&lt;span class="t-type"&gt;${type}&lt;/span&gt;
${childHtml}
&lt;/div&gt;`;
}
return html;
};
container.innerHTML = buildTree(data);
};
window.toggleTree = function(el) {
const children = el.parentElement.querySelector('.t-children');
if (children) {
const isOpen = children.classList.toggle('open');
el.innerText = isOpen ? '▼' : '▶';
}
};
window.copyIdeCode = function(ordinal) {
const el = document.getElementById(`editor-${ordinal}`);
if(el) {
navigator.clipboard.writeText(el.innerText).then(() =&gt; {
const btn = document.getElementById(`copy-code-${ordinal}`);
const old = btn.innerText; btn.innerText = "✓";
setTimeout(() =&gt; { btn.innerText = old; }, 1500);
});
}
};
document.addEventListener("DOMContentLoaded", () =&gt; {
const editorEl = document.getElementById("editor-3");
if (!editorEl) return;
const highlight = (editor) =&gt; {
if (window.Prism &amp;&amp; Prism.languages.python) {
editor.innerHTML = Prism.highlight(editor.textContent, Prism.languages.python, 'python');
}
};
const jar = CodeJar(editorEl, highlight, { tab: ' ' });
try {
const raw = editorEl.getAttribute("data-code");
const decoded = decodeURIComponent(escape(window.atob(raw)));
jar.updateCode(decoded);
} catch (e) { console.error(e); }
editorEl.addEventListener('keydown', (e) =&gt; {
if ((e.ctrlKey || e.metaKey) &amp;&amp; e.key === 'Enter') {
e.preventDefault();
document.getElementById("btn-3").click();
}
});
const copyBtn = document.getElementById("copy-png-btn-3");
if(copyBtn) {
copyBtn.onclick = () =&gt; {
const plot = document.getElementById("plot-3");
const svg = plot.querySelector('svg');
if(!svg) return;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
const data = (new XMLSerializer()).serializeToString(svg);
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(data);
img.onload = () =&gt; {
canvas.width = svg.viewBox.baseVal.width * 2;
canvas.height = svg.viewBox.baseVal.height * 2;
ctx.fillStyle = 'white'; ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.drawImage(img, 0,0,canvas.width,canvas.height);
canvas.toBlob(b =&gt; navigator.clipboard.write([new ClipboardItem({'image/png':b})]));
alert("Plot copied!");
};
};
}
});
&lt;/script&gt;
&lt;script type="py" config='{"packages": ["micropip", "numpy", "matplotlib"]}'&gt;
from pyscript import window, document
import sys, io, micropip, asyncio, html, json, types
if 'kernels' not in globals(): kernels = {}
kernels[3] = {}
class DOMStream:
def __init__(self, element_id): self.id = element_id
def write(self, text):
el = document.getElementById(self.id)
if el: el.innerText += text; el.scrollTop = el.scrollHeight
def flush(self): pass
# 2. 变量树生成器 (恢复 Python 逻辑)
def get_scope_tree(scope):
tree_data = {}
def safe_str(v):
try: s = str(v)
except: s = "&lt;error&gt;"
return html.escape(s[:50] + "..." if len(s) &gt; 50 else s)
for k, v in scope.items():
# 过滤系统变量
if k.startswith("_") or k in ['open', 'exit', 'quit', 'In', 'Out', 'get_ipython', 'show_plot', 'matplotlib', 'plt', 'micropip', 'fm', 'pyfetch', 'os', 'sys', 'io', 'html', 'json', 'types', 'asyncio', 'DOMStream', 'kernels', 'get_scope_tree']: continue
if isinstance(v, (types.ModuleType, types.FunctionType, type)): continue
val_type = type(v).__name__
children = None
display_val = safe_str(v)
# 处理 List/Dict 嵌套
if isinstance(v, dict):
children = {}
display_val = f"dict[{len(v)}]"
for sk, sv in list(v.items())[:20]:
children[str(sk)] = {"val": safe_str(sv), "type": type(sv).__name__, "children": None}
elif isinstance(v, (list, tuple)):
children = {}
display_val = f"{val_type}[{len(v)}]"
for i, sv in enumerate(v[:20]):
children[str(i)] = {"val": safe_str(sv), "type": type(sv).__name__, "children": None}
tree_data[k] = {"val": display_val, "type": val_type, "children": children}
return json.dumps(tree_data)
def _show_plot():
import matplotlib.pyplot as plt
div = document.getElementById("plot-3")
hdr = document.getElementById("plot-header-3")
buf = io.StringIO()
plt.savefig(buf, format='svg', bbox_inches='tight')
div.innerHTML = buf.getvalue()
div.style.display = "block"
hdr.style.display = "flex"
plt.clf()
def reset_code_3(e):
kernels[3] = {"show_plot": _show_plot}
document.getElementById("output-3").innerText = "&gt; Reset.\n"
window.renderJsonTree("tree-3", "{}")
document.getElementById("plot-3").style.display = "none"
document.getElementById("plot-header-3").style.display = "none"
async def run_code_3(e):
btn = document.getElementById("btn-3")
out = document.getElementById("output-3")
editor = document.getElementById("editor-3")
btn.disabled = True
btn.innerText = "..."
out.innerText = ""
# 获取代码
code = window.getSelection().toString()
if not code.strip(): code = editor.textContent
# --- 优化后的安装包逻辑 ---
# 扫描代码中的所有行，寻找以 # install: 开头的行
lines = code.split('\n')
for line in lines:
line = line.strip()
if line.startswith("# install:"):
pkgs = [p.strip() for p in line.replace("# install:", "").split(",") if p.strip()]
if pkgs:
out.innerText += f"&gt; 正在安装依赖: {', '.join(pkgs)}...\n"
try:
await micropip.install(pkgs)
out.innerText += "&gt; 安装成功！\n"
except Exception as err:
out.innerText += f"&gt; 安装失败: {err}\n"
# -----------------------
scope = kernels[3]
if "show_plot" not in scope: scope["show_plot"] = _show_plot
stream = DOMStream("output-3")
sys.stdout = stream
sys.stderr = stream
try:
await asyncio.sleep(0.1)
# 执行代码
exec(code, scope, scope)
except Exception as err:
print(f"Error: {err}")
finally:
sys.stdout = sys.__stdout__
# 渲染变量树
try:
tree = get_scope_tree(scope)
window.renderJsonTree("tree-3", tree)
except: pass
btn.disabled = False
btn.innerText = "▶ RUN"
&lt;/script&gt;
&lt;h2 id="结语"&gt;结语&lt;/h2&gt;
&lt;p&gt;通过这次折腾，我们将一个“只读”的 Hugo 博客变成了一个“可交互”的教学平台。虽然 PyScript 的初始化速度（约 1-2 秒）仍有待提升，但对于 Numpy、Pandas 和基础算法教学，这已经是一个堪称完美的轻量级方案。&lt;/p&gt;
&lt;p&gt;P.S. 记得给浏览器一点时间下载 Python 内核，看到 &amp;ldquo;System Ready&amp;rdquo; 后再点击运行。&lt;/p&gt;</description></item><item><title>⚡️ Turn Jupyter Notebooks into Blog Posts</title><link>https://lin-shang.github.io/blog/notebook-onboarding/</link><pubDate>Mon, 15 Jul 2024 00:00:00 +0800</pubDate><guid>https://lin-shang.github.io/blog/notebook-onboarding/</guid><description>&lt;p&gt;As a researcher or data scientist, your work often lives in Jupyter Notebooks. But sharing those insights effectively usually means taking screenshots, messy copy-pasting, or exporting to PDF.&lt;/p&gt;
&lt;p&gt;Hugo Blox changes that. With the &lt;code&gt;{{&amp;lt; notebook &amp;gt;}}&lt;/code&gt; shortcode, you can render your actual &lt;code&gt;.ipynb&lt;/code&gt; files directly as beautiful, interactive blog posts or project pages. Keep your code, outputs, and narrative in one place.&lt;/p&gt;
&lt;details class="print:hidden xl:hidden" open&gt;
&lt;summary&gt;Table of Contents&lt;/summary&gt;
&lt;div class="text-sm"&gt;
&lt;nav id="TableOfContents"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#why-publish-notebooks"&gt;Why publish notebooks?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#example-data-science-workflow"&gt;Example: Data Science Workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#how-to-add-a-notebook"&gt;How to add a notebook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#next-steps"&gt;Next steps&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="why-publish-notebooks"&gt;Why publish notebooks?&lt;/h2&gt;
&lt;div class="callout flex px-4 py-3 mb-6 rounded-md border-l-4 bg-emerald-100 dark:bg-emerald-900 border-emerald-500"
data-callout="tip"
data-callout-metadata=""&gt;
&lt;span class="callout-icon pr-3 pt-1 text-emerald-600 dark:text-emerald-300"&gt;
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"&gt;&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18"/&gt;&lt;/svg&gt;
&lt;/span&gt;
&lt;div class="callout-content dark:text-neutral-300"&gt;
&lt;div class="callout-title font-semibold mb-1"&gt;Tip&lt;/div&gt;
&lt;div class="callout-body"&gt;&lt;p&gt;&lt;strong&gt;Reproducible Research&lt;/strong&gt;: By publishing the actual notebook, you allow others to download and run your code, verifying your results and building upon your work.&lt;/p&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No more screenshots&lt;/strong&gt; – Render crisp code and vector plots directly from your source.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Theme consistent&lt;/strong&gt; – Notebooks automatically adapt to your site&amp;rsquo;s theme (including dark mode).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible sourcing&lt;/strong&gt; – Display notebooks from your &lt;code&gt;assets/&lt;/code&gt; folder, page bundles, or even directly from a remote GitHub URL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interactive&lt;/strong&gt; – Users can copy code blocks or download the full notebook to run locally.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="example-data-science-workflow"&gt;Example: Data Science Workflow&lt;/h2&gt;
&lt;p&gt;Below is a live example of a notebook rendered right here in this post. Notice how the markdown, code, and outputs (text, HTML, and JSON) are all preserved and styled.&lt;/p&gt;
&lt;div id="hb-notebook-86b2483a72d99021c62bf6617361405b" class="hb-notebook not-prose" data-hb-component="notebook" aria-label="Notebook Launch Readiness Analysis" style="--hb-notebook-output-max-height:26rem;"&gt;
&lt;div class="hb-notebook-header"&gt;
&lt;div class="hb-notebook-heading"&gt;
&lt;p class="hb-notebook-title"&gt;Launch Readiness Analysis&lt;/p&gt;
&lt;p class="hb-notebook-subtitle"&gt;Python · Kernel: Python 3 · nbformat 4.5 · 6 cells&lt;/p&gt;
&lt;/div&gt;
&lt;a class="hb-notebook-download" href="https://lin-shang.github.io/blog/notebook-onboarding/hugoblox-onboarding.ipynb" download&gt;
&lt;svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"&gt;&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/&gt;&lt;/svg&gt;
&lt;span&gt;Download notebook&lt;/span&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;dl class="hb-notebook-metadata"&gt;
&lt;div&gt;
&lt;dt&gt;Language&lt;/dt&gt;
&lt;dd&gt;Python&lt;/dd&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;dt&gt;Version&lt;/dt&gt;
&lt;dd&gt;3.11&lt;/dd&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;dt&gt;Kernel&lt;/dt&gt;
&lt;dd&gt;Python 3 python3&lt;/dd&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;dt&gt;nbformat&lt;/dt&gt;
&lt;dd&gt;4.5&lt;/dd&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;dt&gt;Authors&lt;/dt&gt;
&lt;dd&gt;HugoBlox Studio&lt;/dd&gt;
&lt;/div&gt;
&lt;/dl&gt;&lt;div class="hb-notebook-body"&gt;&lt;article class="hb-notebook-cell hb-notebook-cell--markdown" data-cell-type="markdown"&gt;
&lt;header class="hb-notebook-cell-header"&gt;
&lt;span class="hb-notebook-pill"&gt;Markdown&lt;/span&gt;
&lt;/header&gt;
&lt;div class="hb-notebook-markdown prose dark:prose-invert"&gt;
&lt;h1 id="ship-notebook-stories-in-minutes"&gt;Ship Notebook Stories in Minutes&lt;/h1&gt;
&lt;p&gt;Hugo Blox Notebook renderer turns your &lt;code&gt;.ipynb&lt;/code&gt; experiments into beautiful long-form posts.
Use this sample to see how markdown, code, and outputs flow together.&lt;/p&gt;
&lt;/div&gt;
&lt;/article&gt;&lt;article class="hb-notebook-cell hb-notebook-cell--markdown" data-cell-type="markdown"&gt;
&lt;header class="hb-notebook-cell-header"&gt;
&lt;span class="hb-notebook-pill"&gt;Markdown&lt;/span&gt;
&lt;/header&gt;
&lt;div class="hb-notebook-markdown prose dark:prose-invert"&gt;
&lt;ol&gt;
&lt;li&gt;Drop notebooks inside &lt;code&gt;assets/notebooks/&lt;/code&gt; (or import them as page resources).&lt;/li&gt;
&lt;li&gt;Reference them with &lt;code&gt;{{&amp;lt;/* notebook src=&amp;quot;your.ipynb&amp;quot; */&amp;gt;}}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Control code, outputs, metadata badges, and download links via shortcode params.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/article&gt;&lt;article class="hb-notebook-cell hb-notebook-cell--code" data-cell-type="code"&gt;
&lt;header class="hb-notebook-cell-header"&gt;
&lt;span class="hb-notebook-pill"&gt;In [1]&lt;/span&gt;
&lt;div class="hb-notebook-tags"&gt;
&lt;span&gt;demo&lt;/span&gt;
&lt;span&gt;quickstart&lt;/span&gt;
&lt;/div&gt;
&lt;/header&gt;
&lt;div class="hb-notebook-code" data-language="python"&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;math&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;accuracy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.982&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Collecting data...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Training notebook-ready block...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Done!&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;accuracy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;
&lt;div class="hb-notebook-outputs"&gt;&lt;pre class="hb-notebook-output hb-notebook-output--stream"&gt;&lt;code&gt;Collecting data...
Training notebook-ready block...
Done!
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="hb-notebook-output hb-notebook-output--text"&gt;&lt;code&gt;0.982&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/article&gt;&lt;article class="hb-notebook-cell hb-notebook-cell--code" data-cell-type="code"&gt;
&lt;header class="hb-notebook-cell-header"&gt;
&lt;span class="hb-notebook-pill"&gt;In [2]&lt;/span&gt;
&lt;/header&gt;
&lt;div class="hb-notebook-code" data-language="python"&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;IPython.display&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTML&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;lt;div style=&amp;#39;font-family:Inter,ui-sans-serif;&amp;#39;&amp;gt;&amp;lt;strong&amp;gt;Launch Readiness:&amp;lt;/strong&amp;gt; &amp;lt;span style=&amp;#39;color:#22c55e;&amp;#39;&amp;gt;98.2&lt;/span&gt;&lt;span class="si"&gt;% c&lt;/span&gt;&lt;span class="s2"&gt;onfidence&amp;lt;/span&amp;gt;&amp;lt;br&amp;gt;&amp;lt;em&amp;gt;Notebook blocks are theme-aware and dark-mode friendly.&amp;lt;/em&amp;gt;&amp;lt;/div&amp;gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;
&lt;div class="hb-notebook-outputs"&gt;
&lt;div class="hb-notebook-output hb-notebook-output--html"&gt;&lt;div style='font-family:Inter,ui-sans-serif;'&gt;&lt;strong&gt;Launch Readiness:&lt;/strong&gt; &lt;span style='color:#22c55e;'&gt;98.2% confidence&lt;/span&gt;&lt;br&gt;&lt;em&gt;Notebook blocks are theme-aware and dark-mode friendly.&lt;/em&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/article&gt;&lt;article class="hb-notebook-cell hb-notebook-cell--code" data-cell-type="code"&gt;
&lt;header class="hb-notebook-cell-header"&gt;
&lt;span class="hb-notebook-pill"&gt;In [3]&lt;/span&gt;
&lt;div class="hb-notebook-tags"&gt;
&lt;span&gt;metrics&lt;/span&gt;
&lt;/div&gt;
&lt;/header&gt;
&lt;div class="hb-notebook-code" data-language="python"&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;metrics&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;engagement_rate&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.73&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;read_time_minutes&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;subscribers&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;
&lt;div class="hb-notebook-outputs"&gt;&lt;pre class="hb-notebook-output hb-notebook-output--json"&gt;&lt;code&gt;{
"metrics": {
"engagement_rate": 0.73,
"read_time_minutes": 4.6,
"subscribers": 1280
}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/article&gt;&lt;article class="hb-notebook-cell hb-notebook-cell--markdown" data-cell-type="markdown"&gt;
&lt;header class="hb-notebook-cell-header"&gt;
&lt;span class="hb-notebook-pill"&gt;Markdown&lt;/span&gt;
&lt;/header&gt;
&lt;div class="hb-notebook-markdown prose dark:prose-invert"&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Tip: Pair this block with Call-to-Action cards or the Embed shortcode to link to GitHub repos, datasets, or ARXIV preprints.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/article&gt;
&lt;/div&gt;
&lt;div class="sr-only" data-hb-component="notebook" data-hb-version="1.0" data-hb-license="MIT"&gt;
Powered by Hugo Blox Builder - https://github.com/HugoBlox/hugo-blox-builder
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="how-to-add-a-notebook"&gt;How to add a notebook&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Save your notebook.&lt;/strong&gt; Place your &lt;code&gt;.ipynb&lt;/code&gt; file in &lt;code&gt;assets/notebooks/&lt;/code&gt; (for global access) or inside a page bundle (like &lt;code&gt;content/blog/my-post/analysis.ipynb&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add the shortcode.&lt;/strong&gt; In any Markdown page, simply use:
&lt;code&gt;{{&amp;lt; notebook src=&amp;quot;analysis.ipynb&amp;quot; &amp;gt;}}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Customize.&lt;/strong&gt; You can hide code cells for non-technical audiences (&lt;code&gt;show_code=false&lt;/code&gt;) or just show the output (&lt;code&gt;show_outputs=true&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="callout flex px-4 py-3 mb-6 rounded-md border-l-4 bg-purple-100 dark:bg-purple-900 border-purple-500"
data-callout="important"
data-callout-metadata=""&gt;
&lt;span class="callout-icon pr-3 pt-1 text-purple-600 dark:text-purple-300"&gt;
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"&gt;&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9 3.75h.008v.008H12z"/&gt;&lt;/svg&gt;
&lt;/span&gt;
&lt;div class="callout-content dark:text-neutral-300"&gt;
&lt;div class="callout-title font-semibold mb-1"&gt;Important&lt;/div&gt;
&lt;div class="callout-body"&gt;&lt;p&gt;Hugo Blox respects your privacy. Notebook rendering happens statically at build time—no third-party services required.&lt;/p&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="next-steps"&gt;Next steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Try it out:&lt;/strong&gt; Drop one of your existing notebooks into this site and see how it looks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Link your papers:&lt;/strong&gt; Use the Embed shortcode to link your notebook to your latest arXiv preprint or GitHub repository.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Get help:&lt;/strong&gt; Join the community on
or check the
.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Happy researching! 🚀&lt;/p&gt;</description></item></channel></rss>