<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Flygeonの小站</title><description>Flygeonの小站</description><link>https://flygeon.top/</link><language>zh_CN</language><item><title>博客更新日志</title><link>https://flygeon.top/posts/log/</link><guid isPermaLink="true">https://flygeon.top/posts/log/</guid><description>将会记录博客的更新内容。</description><pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;2026年05月30日&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;卡片标签调整&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2026年05月29日&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;反代bangumi图源，解决大陆地区访问问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2026年05月27日&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;优化bangumi页面布局&lt;/li&gt;
&lt;li&gt;添加线路切换功能&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2026年05月24日&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;添加友链界面&lt;/li&gt;
&lt;li&gt;添加访问量统计&lt;/li&gt;
&lt;li&gt;添加bangumi页面&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>对 Bangumi 反向代理，解决大陆地区访问问题</title><link>https://flygeon.top/posts/4/</link><guid isPermaLink="true">https://flygeon.top/posts/4/</guid><description>使用 Cloudflare Workers 搭建 Bangumi 反向代理，解决大陆地区访问困难的问题。</description><pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::tip
关于为博客添加bangumi页面的教学见上一篇文章：&lt;a href=&quot;/posts/3&quot;&gt;为博客添加bangumi页面&lt;/a&gt;
:::&lt;/p&gt;
&lt;h1&gt;缘由&lt;/h1&gt;
&lt;p&gt;继上篇文章后，我发现这个bangumi在国内访问是一片中国红啊，这就导致了天朝网络的环境下无法加载出图片。为了确保正常访问，就需要搭建一个反向代理，将图片资源从 &lt;code&gt;lain.bgm.tv&lt;/code&gt; 替换为我们自己的。&lt;/p&gt;
&lt;h2&gt;实现原理&lt;/h2&gt;
&lt;p&gt;通过cf worker可以很轻松的实现这个想法，只需要构建好反向代理脚本，然后绑定一个自己的域名即可。(cf默认分配的域名大陆地区会有阻断。)&lt;/p&gt;
&lt;p&gt;不过cf woker在大陆地区的网络情况并不理想，所以我在这基础上又套了一层eo的cdn。&lt;/p&gt;
&lt;p&gt;所以最终流程就是&lt;/p&gt;
&lt;p&gt;&lt;code&gt;用户 -&amp;gt; lain.flygeon.top -&amp;gt; eo节点 -&amp;gt; cfwoker -&amp;gt; lain.bgm.tv&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;为了防止建立反代被cf斩杀，我们还需要移除登录注册相关的敏感路内容。（防止被判钓鱼站点）&lt;/p&gt;
&lt;h2&gt;完整代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Cloudflare Workers 反向代理脚本
 * 支持代理 bgm.tv 和 lain.bgm.tv
 */

// 默认目标域名（主站）
const DEFAULT_HOST = &apos;bgm.tv&apos;;

// 允许代理的目标域名列表
const ALLOWED_HOSTS = [
  &apos;bgm.tv&apos;,
  &apos;lain.bgm.tv&apos;,
];

// 允许的请求方法
const ALLOWED_METHODS = [&apos;GET&apos;, &apos;HEAD&apos;, &apos;POST&apos;, &apos;PUT&apos;, &apos;DELETE&apos;, &apos;OPTIONS&apos;, &apos;PATCH&apos;];

/** 需要屏蔽的固定路径 */
const BLOCKED_PATHS = [
  &apos;/login&apos;,
  &apos;/signup&apos;,
  &apos;/register&apos;,
  &apos;/auth&apos;,
  &apos;/oauth&apos;,
];

/** 需要屏蔽的动态路径规则 */
const BLOCKED_PATTERNS = [
  /^\/subject\/\d+\/edit/,
  /^\/subject\/\d+\/related/,
  /^\/subject\/\d+\/episode_edit/,
  /^\/subject\/\d+\/merge/,
  /^\/character\/\d+\/edit/,
  /^\/person\/\d+\/edit/,
  /^\/subject\/\d+\/\(modify\)/,
  /^\/subject\/\d+\/delete/,
  /^\/subject\/\d+\/recollect/,
  /^\/subject\/\d+\/watched/,
  /^\/subject\/\d+\/interest/,
  /^\/subject\/\d+\/comments/,
];

/** 检查路径是否需要屏蔽 */
function isBlockedPath(path) {
  const normalizedPath = path.replace(/\/$/, &apos;&apos;) || &apos;/&apos;;

  for (const blocked of BLOCKED_PATHS) {
    if (normalizedPath === blocked || normalizedPath.startsWith(blocked + &apos;/&apos;)) {
      return true;
    }
  }

  for (const pattern of BLOCKED_PATTERNS) {
    if (pattern.test(normalizedPath)) {
      return true;
    }
  }

  return false;
}

/** 替换 HTML 中的原始域名为代理域名 */
function rewriteHtmlUrls(html, proxyOrigin) {
  // 替换 lain.bgm.tv 的链接
  html = html.replace(
    /https?:\/\/lain\.bgm\.tv/gi,
    `${proxyOrigin}`
  );

  // 替换 bgm.tv 的链接
  html = html.replace(
    /https?:\/\/bgm\.tv/gi,
    `${proxyOrigin}`
  );

  // 处理相对协议 URL (//lain.bgm.tv)
  html = html.replace(
    /\/\/lain\.bgm\.tv/gi,
    `${proxyOrigin}`
  );

  // 处理相对协议 URL (//bgm.tv)
  html = html.replace(
    /\/\/bgm\.tv/gi,
    `${proxyOrigin}`
  );

  // 避免重复替换：清理可能出现的双重代理 URL
  const escapedOrigin = proxyOrigin.replace(/[.*+?^${}()|[\]\\]/g, &apos;\\$&amp;amp;&apos;);
  const doubleProxyPattern = new RegExp(
    `${escapedOrigin}${escapedOrigin}/`,
    &apos;gi&apos;
  );
  html = html.replace(doubleProxyPattern, `${proxyOrigin}/`);

  return html;
}

/** 过滤 HTML 内容，移除登录/注册相关元素 */
async function filterHtmlContent(response, proxyOrigin) {
  const contentType = response.headers.get(&apos;content-type&apos;) || &apos;&apos;;
  if (!contentType.includes(&apos;text/html&apos;)) {
    return response;
  }

  let html = await response.text();

  // 替换 HTML 中的原始域名为代理域名
  html = rewriteHtmlUrls(html, proxyOrigin);

  // 移除登录/注册相关元素
  const BLOCKED_HTML_PATTERNS = [
    /&amp;lt;form[^&amp;gt;]*action=&quot;[^&quot;]*(?:login|signin)[^&quot;]*&quot;[\s\S]*?&amp;lt;\/form&amp;gt;/gi,
    /&amp;lt;form[^&amp;gt;]*id=&quot;[^&quot;]*login[^&quot;]*&quot;[\s\S]*?&amp;lt;\/form&amp;gt;/gi,
    /&amp;lt;form[^&amp;gt;]*class=&quot;[^&quot;]*login[^&quot;]*&quot;[\s\S]*?&amp;lt;\/form&amp;gt;/gi,
    /&amp;lt;form[^&amp;gt;]*action=&quot;[^&quot;]*(?:signup|register)[^&quot;]*&quot;[\s\S]*?&amp;lt;\/form&amp;gt;/gi,
    /&amp;lt;div[^&amp;gt;]*id=&quot;headerLogin&quot;[^&amp;gt;]*&amp;gt;[\s\S]*?&amp;lt;\/div&amp;gt;/gi,
    /&amp;lt;a[^&amp;gt;]*href=&quot;[^&quot;]*(?:login|signin|signup|register)[^&quot;]*&quot;[^&amp;gt;]*&amp;gt;[\s\S]*?&amp;lt;\/a&amp;gt;/gi,
  ];

  for (const pattern of BLOCKED_HTML_PATTERNS) {
    html = html.replace(pattern, &apos;&apos;);
  }

  const filteredHeaders = new Headers(response.headers);
  filteredHeaders.delete(&apos;content-length&apos;);

  return new Response(html, {
    status: response.status,
    statusText: response.statusText,
    headers: filteredHeaders,
  });
}

/**
 * 主入口
 */
addEventListener(&apos;fetch&apos;, event =&amp;gt; {
  event.respondWith(handleRequest(event.request));
});

/**
 * 处理 OPTIONS 预检请求
 */
function handleOptions(request) {
  const headers = new Headers();
  headers.set(&apos;Access-Control-Allow-Origin&apos;, &apos;*&apos;);
  headers.set(&apos;Access-Control-Allow-Methods&apos;, ALLOWED_METHODS.join(&apos;, &apos;));
  headers.set(&apos;Access-Control-Allow-Headers&apos;, request.headers.get(&apos;Access-Control-Request-Headers&apos;) || &apos;*&apos;);
  headers.set(&apos;Access-Control-Max-Age&apos;, &apos;86400&apos;);

  return new Response(null, {
    status: 204,
    headers: headers,
  });
}

async function handleRequest(request) {
  const url = new URL(request.url);

  // 处理 OPTIONS 预检请求
  if (request.method === &apos;OPTIONS&apos;) {
    return handleOptions(request);
  }

  // 确定目标域名
  let targetHost = DEFAULT_HOST;
  let targetPath = url.pathname;
  let targetSearch = url.search;

  // lain.bgm.tv 特有的资源路径前缀
  const LAIN_PATH_PREFIXES = [&apos;/r/&apos;, &apos;/pic/&apos;, &apos;/cover/&apos;, &apos;/crt/&apos;, &apos;/sub/&apos;, &apos;/user/&apos;];

  if (LAIN_PATH_PREFIXES.some(prefix =&amp;gt; url.pathname.startsWith(prefix))) {
    targetHost = &apos;lain.bgm.tv&apos;;
  }

  // 检查路径是否需要屏蔽
  if (isBlockedPath(targetPath)) {
    return new Response(
      `&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;访问受限&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;
&amp;lt;body style=&quot;text-align:center;padding:50px;font-family:sans-serif;&quot;&amp;gt;
  &amp;lt;h1&amp;gt;访问受限&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;该页面已被屏蔽。&amp;lt;/p&amp;gt;
  &amp;lt;a href=&quot;/&quot;&amp;gt;返回首页&amp;lt;/a&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;`,
      {
        status: 403,
        headers: { &apos;Content-Type&apos;: &apos;text/html; charset=utf-8&apos; },
      }
    );
  }

  // 构建目标 URL
  const targetUrl = `https://${targetHost}${targetPath}${targetSearch}`;

  // 验证目标域名是否在白名单中
  if (!ALLOWED_HOSTS.includes(targetHost)) {
    return new Response(
      JSON.stringify({
        error: &apos;Target host not allowed&apos;,
        allowed_hosts: ALLOWED_HOSTS,
      }),
      {
        status: 403,
        headers: { &apos;Content-Type&apos;: &apos;application/json&apos; },
      }
    );
  }

  // 复制并修改请求头
  const headers = new Headers(request.headers);
  headers.delete(&apos;host&apos;);
  headers.delete(&apos;cf-connecting-ip&apos;);
  headers.delete(&apos;cf-ray&apos;);
  headers.delete(&apos;cf-visitor&apos;);
  headers.delete(&apos;cf-worker&apos;);
  headers.set(&apos;Host&apos;, targetHost);

  // 创建代理请求
  const proxyRequest = new Request(targetUrl, {
    method: request.method,
    headers: headers,
    body: request.body,
    redirect: &apos;manual&apos;,
  });

  try {
    // 发送代理请求
    const response = await fetch(proxyRequest);

    // 复制响应并修改头部
    const proxyHeaders = new Headers(response.headers);
    proxyHeaders.set(&apos;Access-Control-Allow-Origin&apos;, &apos;*&apos;);
    proxyHeaders.set(&apos;Access-Control-Allow-Methods&apos;, ALLOWED_METHODS.join(&apos;, &apos;));
    proxyHeaders.set(&apos;Access-Control-Allow-Headers&apos;, &apos;*&apos;);
    proxyHeaders.delete(&apos;server&apos;);
    proxyHeaders.delete(&apos;x-powered-by&apos;);
    proxyHeaders.delete(&apos;via&apos;);

    // 处理重定向
    if ([301, 302, 303, 307, 308].includes(response.status)) {
      const location = proxyHeaders.get(&apos;location&apos;);
      if (location) {
        try {
          const redirectUrl = new URL(location, targetUrl);
          if (ALLOWED_HOSTS.includes(redirectUrl.hostname)) {
            const proxyRedirect = `${url.origin}${redirectUrl.pathname}${redirectUrl.search}`;
            proxyHeaders.set(&apos;location&apos;, proxyRedirect);
          }
        } catch (e) {
          // 保留原始 location
        }
      }
    }

    // 处理 Set-Cookie 中的 Domain 属性
    const setCookie = proxyHeaders.get(&apos;set-cookie&apos;);
    if (setCookie) {
      proxyHeaders.set(&apos;set-cookie&apos;, setCookie.replace(/domain=[^;]+;?/gi, &apos;&apos;));
    }

    // 过滤 HTML 内容
    const filteredResponse = await filterHtmlContent(
      new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: proxyHeaders,
      }),
      url.origin
    );

    return filteredResponse;

  } catch (error) {
    return new Response(
      JSON.stringify({
        error: &apos;Proxy request failed&apos;,
        message: error.message,
      }),
      {
        status: 502,
        headers: { &apos;Content-Type&apos;: &apos;application/json&apos; },
      }
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;博客 Bangumi 页面适配&lt;/h2&gt;
&lt;p&gt;搭建好反向代理后，还需要修改博客中 Bangumi 页面的图片加载逻辑，让封面图走代理域名而不是直连 &lt;code&gt;lain.bgm.tv&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;修改 Card.astro&lt;/h3&gt;
&lt;p&gt;找到 &lt;code&gt;src/components/bangumi/Card.astro&lt;/code&gt;，添加一个 &lt;code&gt;getImageUrl&lt;/code&gt; 函数，将 Bangumi 的图片地址替换为代理地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import type { UserSubjectCollection } from &quot;@/types/bangumi&quot;;

interface Props {
	item: UserSubjectCollection;
}

const { item } = Astro.props;

const subject_base_url = &quot;https://bgm.tv/subject/&quot;;

function getImageUrl(originalUrl: string | undefined): string | undefined {
	if (!originalUrl) return undefined;
	return originalUrl.replace(
		&quot;https://lain.bgm.tv/&quot;,
		&quot;https://lain.flygeon.top/&quot;,
	);
}
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在图片标签中使用这个函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img
  data-src={getImageUrl(item.subject.images.medium)}
  alt={item.subject.name_cn || item.subject.name}
  class=&quot;bangumi-lazy-img w-full h-full object-cover pointer-events-none opacity-0 transition-opacity duration-300&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么只改 Card 组件&lt;/h3&gt;
&lt;p&gt;Bangumi 页面中，图片地址只出现在 &lt;code&gt;Card.astro&lt;/code&gt; 这一个组件里。&lt;code&gt;BangumiSection.astro&lt;/code&gt; 负责数据分发和筛选，&lt;code&gt;Pagination.astro&lt;/code&gt; 负责分页交互，&lt;code&gt;FilterControls.astro&lt;/code&gt; 负责状态过滤，它们都不涉及图片 URL 的渲染。因此只需要修改 Card 组件即可覆盖所有图片请求。&lt;/p&gt;
&lt;h2&gt;enjoy it！&lt;/h2&gt;
</content:encoded></item><item><title>为 Fuwari 博客添加 Umami 访问量统计</title><link>https://flygeon.top/posts/3/</link><guid isPermaLink="true">https://flygeon.top/posts/3/</guid><description>本文介绍如何基于 Umami 逆向 API 为静态博客添加实时访问量统计，包括侧边栏总览卡片和文章独立浏览量。</description><pubDate>Sun, 24 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::tip
你可以直接将这篇文章丢给你的ai Agent，让它帮你实现。
:::&lt;/p&gt;
&lt;h1&gt;为 Fuwari 博客添加 Umami 访问量统计&lt;/h1&gt;
&lt;p&gt;对于静态博客而言，了解访客来源与流量趋势是很有必要的。在不自建服务器的前提下，配置简单、功能强大的 Umami 往往是我们的首选。&lt;/p&gt;
&lt;p&gt;然而，直接挂载 Umami 的分享外链不够直观且破坏页面一致性。本文将教你如何将 Umami 的统计数据以原生组件的形式集成到博客中，实现以下效果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;侧边栏显示&lt;strong&gt;总浏览量&lt;/strong&gt;和&lt;strong&gt;访问数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;每篇文章卡片显示该文章的&lt;strong&gt;独立浏览量&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;数据实时获取，支持数字动画&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;前置条件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;一个已部署好的 Fuwari 博客&lt;/li&gt;
&lt;li&gt;启用 Umami 统计的站点&lt;/li&gt;
&lt;li&gt;获取 Umami 的分享链接&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;获取 Umami 分享链接&lt;/h3&gt;
&lt;p&gt;在 Umami 后台启用&quot;分享 URL&quot;，你应该会得到一个类似 &lt;code&gt;https://umami.example.com/share/OSwi3PpuZgZ2Io3a&lt;/code&gt; 格式的链接。记下这个链接，稍后配置需要用到。&lt;/p&gt;
&lt;h2&gt;步骤一：添加 Umami 跟踪脚本&lt;/h2&gt;
&lt;p&gt;首先需要在博客的 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; 中注入 Umami 的跟踪脚本，这样才能记录访问数据。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;src/layouts/Layout.astro&lt;/code&gt; 的 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; 区域添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script defer src=&quot;https://example.com/script.js&quot; data-website-id=&quot;你的website-id&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：这在Umami的后台配置中可以找到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;步骤二：创建侧边栏统计组件&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;src/components/widget/&lt;/code&gt; 目录下创建 &lt;code&gt;UmamiStats.astro&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import WidgetLayout from &quot;./WidgetLayout.astro&quot;;

interface Props {
	class?: string;
	style?: string;
}
const { class: className, style } = Astro.props;
---

&amp;lt;WidgetLayout name=&quot;统计&quot; id=&quot;umami-stats&quot; class:list={[className, &quot;cursor-pointer transition-opacity active:scale-95&quot;]} {style}&amp;gt;
    &amp;lt;a id=&quot;umami-link&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot; class=&quot;block&quot;&amp;gt;
        &amp;lt;div class=&quot;grid grid-cols-2 divide-x divide-neutral-200 dark:divide-neutral-700 text-center py-2&quot;&amp;gt;
            &amp;lt;div class=&quot;px-2&quot;&amp;gt;
                &amp;lt;div class=&quot;text-2xl font-bold text-neutral-900 dark:text-neutral-100&quot; id=&quot;total-pageviews&quot;&amp;gt;-&amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;text-sm text-neutral-500 dark:text-neutral-400&quot;&amp;gt;总浏览量&amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;px-2&quot;&amp;gt;
                &amp;lt;div class=&quot;text-2xl font-bold text-neutral-900 dark:text-neutral-100&quot; id=&quot;total-visits&quot;&amp;gt;-&amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;text-sm text-neutral-500 dark:text-neutral-400&quot;&amp;gt;访问数&amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/a&amp;gt;
&amp;lt;/WidgetLayout&amp;gt;

&amp;lt;script&amp;gt;
const UMAMI_CONFIG = {
    apiUrl: &apos;https://umami.flygeon.eu.org/api&apos;,
    shareId: &apos;OSwi3PpuZgZ2Io3a&apos;,
};

let __UMAMI_INTERNAL = {
    websiteId: &apos;&apos;,
    shareToken: &apos;&apos;,
    isReady: false
};

const FALLBACK_STATS = {
    pageviews: 1000,
    visits: 1000,
};

async function initUmamiConfig() {
    try {
        const res = await fetch(`${UMAMI_CONFIG.apiUrl}/share/${UMAMI_CONFIG.shareId}`);
        const data = await res.json();

        __UMAMI_INTERNAL = {
            websiteId: data.websiteId,
            shareToken: data.token,
            isReady: true
        };

        const link = document.getElementById(&apos;umami-link&apos;);
        if (link) link.setAttribute(&apos;href&apos;, `https://umami.flygeon.eu.org/share/${UMAMI_CONFIG.shareId}`);

    } catch (e) {
        console.error(&apos;Umami Config Init Failed:&apos;, e);
    }
}

function formatNumber(num: number): string {
    if (num &amp;gt;= 1000000) {
        return (num / 1000000).toFixed(1) + &apos;M&apos;;
    } else if (num &amp;gt;= 1000) {
        return (num / 1000).toFixed(1) + &apos;K&apos;;
    }
    return Math.round(num).toString();
}

function setStats(values: { pageviews: number; visits: number }) {
    const pageviewsElement = document.getElementById(&apos;total-pageviews&apos;);
    const visitsElement = document.getElementById(&apos;total-visits&apos;);

    const easeOutCubic = (t: number) =&amp;gt; 1 - Math.pow(1 - t, 3);
    const animHandles = new Map&amp;lt;HTMLElement, number&amp;gt;();

    const animateStat = (el: HTMLElement | null, to: number, duration = 2000) =&amp;gt; {
        if (!el) return;

        const prev = animHandles.get(el);
        if (prev) cancelAnimationFrame(prev);

        const from = 0;
        const startTime = performance.now();

        const tick = (now: number) =&amp;gt; {
            const elapsed = now - startTime;
            const progress = Math.min(1, elapsed / duration);
            const easedProgress = easeOutCubic(progress);

            const current = from + (to - from) * easedProgress;
            el.textContent = formatNumber(current);

            if (progress &amp;lt; 1) {
                animHandles.set(el, requestAnimationFrame(tick));
            }
        };
        animHandles.set(el, requestAnimationFrame(tick));
    };

    animateStat(pageviewsElement, values.pageviews);
    animateStat(visitsElement, values.visits);
}

async function fetchUmamiStats() {
    if (!__UMAMI_INTERNAL.isReady) {
        await initUmamiConfig();
    }

    if (!__UMAMI_INTERNAL.isReady) {
        setStats(FALLBACK_STATS);
        return;
    }

    try {
        const endAt = Date.now();
        const startAt = 0;
        const url = `${UMAMI_CONFIG.apiUrl}/websites/${__UMAMI_INTERNAL.websiteId}/stats?startAt=${startAt}&amp;amp;endAt=${endAt}&amp;amp;unit=hour&amp;amp;timezone=Asia%2FShanghai`;

        const response = await fetch(url, {
            headers: {
                &apos;x-umami-share-token&apos;: __UMAMI_INTERNAL.shareToken
            }
        });

        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data = await response.json();

        setStats({
            pageviews: data.pageviews || 0,
            visits: data.visits || 0,
        });

    } catch (error) {
        console.error(&apos;Umami Fetch Failed:&apos;, error);
        setStats(FALLBACK_STATS);
    }
}

let __umamiStatsStarted = false;
function startUmamiStats() {
    if (__umamiStatsStarted) return;
    __umamiStatsStarted = true;
    fetchUmamiStats();
}

function initUmamiStatsVisibility() {
    const container = document.getElementById(&apos;umami-stats&apos;);
    const io = new IntersectionObserver((entries) =&amp;gt; {
        if (entries[0].isIntersecting) {
            startUmamiStats();
            io.disconnect();
        }
    }, { threshold: 0.1 });

    if (container) io.observe(container);
}

initUmamiStatsVisibility();

if (window.swup) {
    window.swup.hooks.on(&apos;page:view&apos;, () =&amp;gt; {
        __umamiStatsStarted = false;
        initUmamiStatsVisibility();
    });
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记得将 &lt;code&gt;apiUrl&lt;/code&gt; 和 &lt;code&gt;shareId&lt;/code&gt; 替换为你自己的 Umami 信息，如果直接照抄我的配置，你的网站统计会算在我博客上。&lt;/p&gt;
&lt;p&gt;我是自建umami服务器的，如果你使用Umami官方的服务，你的 &lt;code&gt;apiUrl&lt;/code&gt; 应该是&lt;code&gt;https://cloud.umami.is/analytics/地区/api&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;美国的是us，欧洲的是eu&lt;/p&gt;
&lt;p&gt;如果你和我一样自建服务器的话，地址则是&lt;code&gt;https://服务器地址/api&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;步骤三：将统计组件添加到侧边栏&lt;/h2&gt;
&lt;p&gt;编辑 &lt;code&gt;src/components/widget/SideBar.astro&lt;/code&gt;，导入并使用 &lt;code&gt;UmamiStats&lt;/code&gt; 组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import type { MarkdownHeading } from &quot;astro&quot;;
import Categories from &quot;./Categories.astro&quot;;
import Profile from &quot;./Profile.astro&quot;;
import RouteSwitch from &quot;./RouteSwitch.astro&quot;;
import Tag from &quot;./Tags.astro&quot;;
import UmamiStats from &quot;./UmamiStats.astro&quot;;

interface Props {
	class?: string;
	headings?: MarkdownHeading[];
}

const className = Astro.props.class;
---
&amp;lt;div id=&quot;sidebar&quot; class:list={[className, &quot;w-full&quot;]}&amp;gt;
    &amp;lt;div class=&quot;flex flex-col w-full gap-4 mb-4&quot;&amp;gt;
        &amp;lt;Profile&amp;gt;&amp;lt;/Profile&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div id=&quot;sidebar-sticky&quot; class=&quot;transition-all duration-700 flex flex-col w-full gap-4 top-4 sticky top-4&quot;&amp;gt;
        &amp;lt;RouteSwitch class=&quot;onload-animation&quot; style=&quot;animation-delay: 100ms&quot;&amp;gt;&amp;lt;/RouteSwitch&amp;gt;
        &amp;lt;Categories class=&quot;onload-animation&quot; style=&quot;animation-delay: 150ms&quot;&amp;gt;&amp;lt;/Categories&amp;gt;
        &amp;lt;Tag class=&quot;onload-animation&quot; style=&quot;animation-delay: 200ms&quot;&amp;gt;&amp;lt;/Tag&amp;gt;
        &amp;lt;UmamiStats class=&quot;onload-animation&quot; style=&quot;animation-delay: 250ms&quot;&amp;gt;&amp;lt;/UmamiStats&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;步骤四：为文章卡片添加独立浏览量&lt;/h2&gt;
&lt;p&gt;编辑 &lt;code&gt;src/components/PostCard.astro&lt;/code&gt;，在 frontmatter 区域添加文章路径提取：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const postPath = url.replace(/\/$/, &quot;&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在文章卡片的元信息区域（字数和阅读时间旁边）添加浏览量显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;text-sm text-black/30 dark:text-white/30 flex gap-4 transition&quot;&amp;gt;
    &amp;lt;div&amp;gt;
        {remarkPluginFrontmatter.words} {&quot; &quot; + i18n(remarkPluginFrontmatter.words === 1 ? I18nKey.wordCount : I18nKey.wordsCount)}
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;|&amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        {remarkPluginFrontmatter.minutes} {&quot; &quot; + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;|&amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;flex items-center gap-1 post-views&quot; data-path={postPath}&amp;gt;
        &amp;lt;Icon name=&quot;material-symbols:visibility-outline-rounded&quot; class=&quot;text-base&quot;&amp;gt;&amp;lt;/Icon&amp;gt;
        &amp;lt;span class=&quot;post-views-count&quot;&amp;gt;-&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在文件末尾添加统计脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
const UMAMI_POST_CONFIG = {
    apiUrl: &apos;https://umami.flygeon.eu.org/api&apos;,
    shareId: &apos;OSwi3PpuZgZ2Io3a&apos;,
};

let __UMAMI_POST_INTERNAL = {
    websiteId: &apos;&apos;,
    shareToken: &apos;&apos;,
    isReady: false
};

async function initUmamiPostConfig() {
    try {
        const res = await fetch(`${UMAMI_POST_CONFIG.apiUrl}/share/${UMAMI_POST_CONFIG.shareId}`);
        const data = await res.json();

        __UMAMI_POST_INTERNAL = {
            websiteId: data.websiteId,
            shareToken: data.token,
            isReady: true
        };
    } catch (e) {
        console.error(&apos;Umami Post Config Init Failed:&apos;, e);
    }
}

function formatNumber(num: number): string {
    if (num &amp;gt;= 1000000) {
        return (num / 1000000).toFixed(1) + &apos;M&apos;;
    } else if (num &amp;gt;= 1000) {
        return (num / 1000).toFixed(1) + &apos;K&apos;;
    }
    return Math.round(num).toString();
}

async function fetchPostStats(path: string) {
    if (!__UMAMI_POST_INTERNAL.isReady) {
        await initUmamiPostConfig();
    }

    if (!__UMAMI_POST_INTERNAL.isReady) {
        return null;
    }

    try {
        const endAt = Date.now();
        const startAt = 0;
        // Umami v3 API: use path=eq. prefix for exact match
        const apiUrl = `${UMAMI_POST_CONFIG.apiUrl}/websites/${__UMAMI_POST_INTERNAL.websiteId}/stats?startAt=${startAt}&amp;amp;endAt=${endAt}&amp;amp;unit=hour&amp;amp;timezone=Asia%2FShanghai&amp;amp;path=eq.${encodeURIComponent(path + &apos;/&apos;)}`;

        const response = await fetch(apiUrl, {
            headers: {
                &apos;x-umami-share-token&apos;: __UMAMI_POST_INTERNAL.shareToken
            }
        });

        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data = await response.json();

        return {
            pageviews: data.pageviews || 0,
            visits: data.visits || 0,
        };
    } catch (error) {
        console.error(&apos;Umami Post Fetch Failed:&apos;, error);
        return null;
    }
}

async function initPostViews() {
    const viewElements = document.querySelectorAll(&apos;.post-views&apos;);
    if (viewElements.length === 0) return;

    for (const el of viewElements) {
        const path = el.getAttribute(&apos;data-path&apos;);
        const countEl = el.querySelector(&apos;.post-views-count&apos;);
        if (!path || !countEl) continue;

        const stats = await fetchPostStats(path);
        if (stats) {
            countEl.textContent = formatNumber(stats.pageviews);
        } else {
            countEl.textContent = &apos;-&apos;;
        }
    }
}

initPostViews();

if (window.swup) {
    window.swup.hooks.on(&apos;page:view&apos;, () =&amp;gt; {
        initPostViews();
    });
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自定义选项&lt;/h2&gt;
&lt;h3&gt;修改统计周期&lt;/h3&gt;
&lt;p&gt;默认统计&lt;strong&gt;所有时间&lt;/strong&gt;的数据（&lt;code&gt;startAt = 0&lt;/code&gt;）。如需调整时间范围，可修改 &lt;code&gt;startAt&lt;/code&gt; 参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 最近24小时
const startAt = Date.now() - 86400000;

// 最近30天
const startAt = Date.now() - 2592000000;

// 最近90天
const startAt = Date.now() - 7776000000;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;修改备用数据&lt;/h3&gt;
&lt;p&gt;如果 API 请求失败，组件会显示备用数据。可在 &lt;code&gt;FALLBACK_STATS&lt;/code&gt; 中修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const FALLBACK_STATS = {
    pageviews: 1000,
    visits: 1000,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最终效果&lt;/h2&gt;
&lt;p&gt;完成以上步骤后，你将看到：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;侧边栏统计卡片&lt;/strong&gt;：实时显示博客总浏览量和总访问数，点击可跳转到 Umami 分享页&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文章卡片浏览量&lt;/strong&gt;：每篇文章显示独立的浏览量数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数字动画&lt;/strong&gt;：数据加载时带有平滑的增长动画&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SPA 兼容&lt;/strong&gt;：使用 Swup 路由时数据会自动刷新&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;参考文献&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.tianhw.top/posts/fuwari-umami-stats/&quot;&gt;为 Fuwari 添加 Umami 访问统计卡片&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://2x.nz/posts/static-view/&quot;&gt;静态博客展示文章浏览量&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>一些mmd资源/网站汇总</title><link>https://flygeon.top/posts/2/</link><guid isPermaLink="true">https://flygeon.top/posts/2/</guid><description>最近在学习制作mmd，刚好把一些用到的东西贴在这里备忘</description><pubDate>Sat, 23 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;模型站&lt;/h1&gt;
&lt;p&gt;模之屋 &lt;a href=&quot;https://www.aplaybox.com/&quot;&gt;点我&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;N站 &lt;a href=&quot;https://www.niconico.jp/&quot;&gt;点我&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;软件下载&lt;/h1&gt;
&lt;p&gt;mmd本体整合：&lt;a href=&quot;https://oss.aplaybox.com/uploads/works/models/MMD2025%E6%95%B4%E5%90%88%E7%89%88.zip&quot;&gt;点我&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;bledner mmd插件：&lt;a href=&quot;https://extensions.blender.org/add-ons/mmd-tools/&quot;&gt;点我&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;pmxeditor：&lt;/p&gt;
&lt;h1&gt;教程&lt;/h1&gt;
&lt;p&gt;改模教程：&lt;a href=&quot;https://www.iwara.tv/forum/guides/5d92a1db-49b4-48f0-95dc-482b4b8ceffd/r18-chinese-tutorial-how-to-edit-a-model-into-nsfw-version&quot;&gt;点我&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;模之屋教程：&lt;a href=&quot;https://www.aplaybox.com/article/details/749211379&quot;&gt;点我&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;PmxEditor从入门到精通：&lt;a href=&quot;https://www.bilibili.com/video/BV1oW411G7ri/&quot;&gt;点我&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>新的开始</title><link>https://flygeon.top/posts/1/</link><guid isPermaLink="true">https://flygeon.top/posts/1/</guid><description>RE:第一章 堂堂复活 Stand by！</description><pubDate>Wed, 20 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;复活YADAZE 👋&lt;/h1&gt;
&lt;p&gt;孩子们博客复活了，这次换了架构，由之前的Typecho切换到了Astro。&lt;/p&gt;
&lt;p&gt;其实很久之前就说要换了，但是，bokuwa 比较懒狗，&lt;/p&gt;
&lt;p&gt;正好vps也快到期了，所以。。。&lt;/p&gt;
&lt;p&gt;之前的文章可能会搬过来吧，也可能不会，这也说不准&lt;/p&gt;
&lt;p&gt;之前的文章图片用的都是图床，图炸了好多，补起来有点麻烦&lt;/p&gt;
&lt;p&gt;就这样吧。&lt;/p&gt;
</content:encoded></item><item><title>Markdown Extended Features</title><link>https://flygeon.top/posts/markdown-extended/</link><guid isPermaLink="true">https://flygeon.top/posts/markdown-extended/</guid><description>Read more about Markdown features in Fuwari</description><pubDate>Wed, 01 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;GitHub Repository Cards&lt;/h2&gt;
&lt;p&gt;You can add dynamic cards that link to GitHub repositories, on page load, the repository information is pulled from the GitHub API.&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;Fabrizz/MMM-OnSpotify&quot;}&lt;/p&gt;
&lt;p&gt;Create a GitHub repository card with the code &lt;code&gt;::github{repo=&quot;&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;&quot;}&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::github{repo=&quot;saicaca/fuwari&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Admonitions&lt;/h2&gt;
&lt;p&gt;Following types of admonitions are supported: &lt;code&gt;note&lt;/code&gt; &lt;code&gt;tip&lt;/code&gt; &lt;code&gt;important&lt;/code&gt; &lt;code&gt;warning&lt;/code&gt; &lt;code&gt;caution&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;:::note
Highlights information that users should take into account, even when skimming.
:::&lt;/p&gt;
&lt;p&gt;:::tip
Optional information to help a user be more successful.
:::&lt;/p&gt;
&lt;p&gt;:::important
Crucial information necessary for users to succeed.
:::&lt;/p&gt;
&lt;p&gt;:::warning
Critical content demanding immediate user attention due to potential risks.
:::&lt;/p&gt;
&lt;p&gt;:::caution
Negative potential consequences of an action.
:::&lt;/p&gt;
&lt;h3&gt;Basic Syntax&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;:::note
Highlights information that users should take into account, even when skimming.
:::

:::tip
Optional information to help a user be more successful.
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Custom Titles&lt;/h3&gt;
&lt;p&gt;The title of the admonition can be customized.&lt;/p&gt;
&lt;p&gt;:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GitHub Syntax&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;a href=&quot;https://github.com/orgs/community/discussions/16925&quot;&gt;The GitHub syntax&lt;/a&gt; is also supported.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [!NOTE]
&amp;gt; The GitHub syntax is also supported.

&amp;gt; [!TIP]
&amp;gt; The GitHub syntax is also supported.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Spoiler&lt;/h3&gt;
&lt;p&gt;You can add spoilers to your text. The text also supports &lt;strong&gt;Markdown&lt;/strong&gt; syntax.&lt;/p&gt;
&lt;p&gt;The content :spoiler[is hidden &lt;strong&gt;ayyy&lt;/strong&gt;]!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The content :spoiler[is hidden **ayyy**]!

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Expressive Code Example</title><link>https://flygeon.top/posts/expressive-code/</link><guid isPermaLink="true">https://flygeon.top/posts/expressive-code/</guid><description>How code blocks look in Markdown using Expressive Code.</description><pubDate>Wed, 10 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Here, we&apos;ll explore how code blocks look using &lt;a href=&quot;https://expressive-code.com/&quot;&gt;Expressive Code&lt;/a&gt;. The provided examples are based on the official documentation, which you can refer to for further details.&lt;/p&gt;
&lt;h2&gt;Expressive Code&lt;/h2&gt;
&lt;h3&gt;Syntax Highlighting&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/syntax-highlighting/&quot;&gt;Syntax Highlighting&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Regular syntax highlighting&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;This code is syntax highlighted!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Rendering ANSI escape sequences&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;ANSI colors:
- Regular: [31mRed[0m [32mGreen[0m [33mYellow[0m [34mBlue[0m [35mMagenta[0m [36mCyan[0m
- Bold:    [1;31mRed[0m [1;32mGreen[0m [1;33mYellow[0m [1;34mBlue[0m [1;35mMagenta[0m [1;36mCyan[0m
- Dimmed:  [2;31mRed[0m [2;32mGreen[0m [2;33mYellow[0m [2;34mBlue[0m [2;35mMagenta[0m [2;36mCyan[0m

256 colors (showing colors 160-177):
[38;5;160m160 [38;5;161m161 [38;5;162m162 [38;5;163m163 [38;5;164m164 [38;5;165m165[0m
[38;5;166m166 [38;5;167m167 [38;5;168m168 [38;5;169m169 [38;5;170m170 [38;5;171m171[0m
[38;5;172m172 [38;5;173m173 [38;5;174m174 [38;5;175m175 [38;5;176m176 [38;5;177m177[0m

Full RGB colors:
[38;2;34;139;34mForestGreen - RGB(34, 139, 34)[0m

Text formatting: [1mBold[0m [2mDimmed[0m [3mItalic[0m [4mUnderline[0m
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Editor &amp;amp; Terminal Frames&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/frames/&quot;&gt;Editor &amp;amp; Terminal Frames&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Code editor frames&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Title attribute example&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- src/content/index.html --&amp;gt;
&amp;lt;div&amp;gt;File name comment example&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Terminal frames&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;This terminal frame has no title&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;Write-Output &quot;This one has a title!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Overriding frame types&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Look ma, no frame!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;# Without overriding, this would be a terminal frame
function Watch-Tail { Get-Content -Tail 20 -Wait $args }
New-Alias tail Watch-Tail
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Text &amp;amp; Line Markers&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/text-markers/&quot;&gt;Text &amp;amp; Line Markers&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Marking full lines &amp;amp; line ranges&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 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 &quot;7-8&quot;
// Line 8 - targeted by range &quot;7-8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Selecting line marker types (mark, ins, del)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;this line is marked as deleted&apos;)
  // This line and the next one are marked as inserted
  console.log(&apos;this is the second inserted line&apos;)

  return &apos;this line uses the neutral default marker type&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Adding labels to line markers&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}
  value={value}
  className={buttonClassName}
  disabled={disabled}
  active={active}
&amp;gt;
  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Adding long labels on their own lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}

  value={value}
  className={buttonClassName}

  disabled={disabled}
  active={active}
&amp;gt;

  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Using diff-like syntax&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;+this line will be marked as inserted
-this line will be marked as deleted
this is a regular line
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+this is an actual diff file
-all contents will remain unmodified
 no whitespace will be removed either
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Combining syntax highlighting with diff-like syntax&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;  function thisIsJavaScript() {
    // This entire block gets highlighted as JavaScript,
    // and we can still add diff markers to it!
-   console.log(&apos;Old code to be removed&apos;)
+   console.log(&apos;New and shiny code!&apos;)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Marking individual text inside lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  // Mark any given text inside lines
  return &apos;Multiple matches of the given text are supported&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Regular expressions&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;The words yes and yep will be marked.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Escaping forward slashes&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Test&quot; &amp;gt; /home/test.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Selecting inline marker types (mark, ins, del)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;These are inserted and deleted marker types&apos;);
  // The return statement uses the default marker type
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Word Wrap&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/word-wrap/&quot;&gt;Word Wrap&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Configuring word wrap per block&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Example with wrap
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Example with wrap=false
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Configuring indentation of wrapped lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Example with preserveIndent (enabled by default)
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Example with preserveIndent=false
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Collapsible Sections&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/collapsible-sections/&quot;&gt;Collapsible Sections&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// All this boilerplate setup code will be collapsed
import { someBoilerplateEngine } from &apos;@example/some-boilerplate&apos;
import { evenMoreBoilerplate } from &apos;@example/even-more-boilerplate&apos;

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: &apos;End of example boilerplate code&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Line Numbers&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/line-numbers/&quot;&gt;Line Numbers&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Displaying line numbers per block&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// This code block will show line numbers
console.log(&apos;Greetings from line 2!&apos;)
console.log(&apos;I am on line 3&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Line numbers are disabled for this block
console.log(&apos;Hello?&apos;)
console.log(&apos;Sorry, do you know what line I am on?&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Changing the starting line number&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Greetings from line 5!&apos;)
console.log(&apos;I am on line 6&apos;)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Markdown Example</title><link>https://flygeon.top/posts/markdown/</link><guid isPermaLink="true">https://flygeon.top/posts/markdown/</guid><description>A simple example of a Markdown blog post.</description><pubDate>Sun, 01 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;An h1 header&lt;/h1&gt;
&lt;p&gt;Paragraphs are separated by a blank line.&lt;/p&gt;
&lt;p&gt;2nd paragraph. &lt;em&gt;Italic&lt;/em&gt;, &lt;strong&gt;bold&lt;/strong&gt;, and &lt;code&gt;monospace&lt;/code&gt;. Itemized lists
look like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this one&lt;/li&gt;
&lt;li&gt;that one&lt;/li&gt;
&lt;li&gt;the other one&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that --- not considering the asterisk --- the actual text
content starts at 4-columns in.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Block quotes are
written like so.&lt;/p&gt;
&lt;p&gt;They can span multiple paragraphs,
if you like.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., &quot;it&apos;s all
in chapters 12--14&quot;). Three dots ... will be converted to an ellipsis.
Unicode is supported. ☺&lt;/p&gt;
&lt;h2&gt;An h2 header&lt;/h2&gt;
&lt;p&gt;Here&apos;s a numbered list:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;first item&lt;/li&gt;
&lt;li&gt;second item&lt;/li&gt;
&lt;li&gt;third item&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note again how the actual text starts at 4 columns in (4 characters
from the left side). Here&apos;s a code sample:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Let me re-iterate ...
for i in 1 .. 10 { do-something(i) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you probably guessed, indented 4 spaces. By the way, instead of
indenting the block, you can use delimited blocks, if you like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;define foobar() {
    print &quot;Welcome to flavor country!&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(which makes copying &amp;amp; pasting easier). You can optionally mark the
delimited block for Pandoc to syntax highlight it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
# Quick, count to ten!
for i in range(10):
    # (but not *too* quick)
    time.sleep(0.5)
    print i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;An h3 header&lt;/h3&gt;
&lt;p&gt;Now a nested list:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;First, get these ingredients:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;carrots&lt;/li&gt;
&lt;li&gt;celery&lt;/li&gt;
&lt;li&gt;lentils&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Boil some water.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dump everything in the pot and follow
this algorithm:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; find wooden spoon
 uncover pot
 stir
 cover pot
 balance wooden spoon precariously on pot handle
 wait 10 minutes
 goto first step (or shut off burner when done)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not bump wooden spoon or it will fall.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice again how text always lines up on 4-space indents (including
that last line which continues item 3 above).&lt;/p&gt;
&lt;p&gt;Here&apos;s a link to &lt;a href=&quot;http://foo.bar&quot;&gt;a website&lt;/a&gt;, to a &lt;a href=&quot;local-doc.html&quot;&gt;local
doc&lt;/a&gt;, and to a &lt;a href=&quot;#an-h2-header&quot;&gt;section heading in the current
doc&lt;/a&gt;. Here&apos;s a footnote [^1].&lt;/p&gt;
&lt;p&gt;[^1]: Footnote text goes here.&lt;/p&gt;
&lt;p&gt;Tables can look like this:&lt;/p&gt;
&lt;p&gt;size material color&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;9 leather brown
10 hemp canvas natural
11 glass transparent&lt;/p&gt;
&lt;p&gt;Table: Shoes, their sizes, and what they&apos;re made of&lt;/p&gt;
&lt;p&gt;(The above is the caption for the table.) Pandoc also supports
multi-line tables:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;keyword text&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;red Sunsets, apples, and
other red or reddish
things.&lt;/p&gt;
&lt;p&gt;green Leaves, grass, frogs
and other things it&apos;s
not easy being.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;A horizontal rule follows.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Here&apos;s a definition list:&lt;/p&gt;
&lt;p&gt;apples
: Good for making applesauce.
oranges
: Citrus!
tomatoes
: There&apos;s no &quot;e&quot; in tomatoe.&lt;/p&gt;
&lt;p&gt;Again, text is indented 4 spaces. (Put a blank line between each
term/definition pair to spread things out more.)&lt;/p&gt;
&lt;p&gt;Here&apos;s a &quot;line block&quot;:&lt;/p&gt;
&lt;p&gt;| Line one
| Line too
| Line tree&lt;/p&gt;
&lt;p&gt;and images can be specified like so:&lt;/p&gt;
&lt;p&gt;Inline math equations go in like so: $\omega = d\phi / dt$. Display
math should get its own line and be put in in double-dollarsigns:&lt;/p&gt;
&lt;p&gt;$$I = \int \rho R^{2} dV$$&lt;/p&gt;
&lt;p&gt;$$
\begin{equation*}
\pi
=3.1415926535
;8979323846;2643383279;5028841971;6939937510;5820974944
;5923078164;0628620899;8628034825;3421170679;\ldots
\end{equation*}
$$&lt;/p&gt;
&lt;p&gt;And note that you can backslash-escape any punctuation characters
which you wish to be displayed literally, ex.: `foo`, *bar*, etc.&lt;/p&gt;
</content:encoded></item><item><title>Include Video in the Posts</title><link>https://flygeon.top/posts/video/</link><guid isPermaLink="true">https://flygeon.top/posts/video/</guid><description>This post demonstrates how to include embedded video in a blog post.</description><pubDate>Tue, 01 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Just copy the embed code from YouTube or other platforms, and paste it in the markdown file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: Include Video in the Post
published: 2023-10-19
// ...
---

&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;YouTube&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;Bilibili&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?bvid=BV1fK4y1s7Qf&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded></item></channel></rss>