作者: Zeeko

  • 如何为 CodeMirror Widget 添加 lint marker

    CodeMirror6 中可以创建 Atomic Rranges 用自定义 Widget 替换文本,从而实现更加丰富的展示效果。但是当文本被 Widget 替换后,lint 功能的 markClass 并不会被添加到生成的 Widget 元素上,这就会导致用户无法在 Widget 上看到 linter 生成的错误标记。

    CodeMirror 并没有提供直接地解决方案,我们需要自己将 lint 结果绑定到 Widget 上。由于 Widget 的渲染时机跟 linter 的运行时间都是黑盒,可以假设这俩都会在编辑器内容更新后被异步并发调用。为了方便将 linter 异步产生的结果同步给 Widget ,可以用 rxjs 的 BehaviorSubject 存储 linter 的结果。

    import { Diagnostic } from '@codemirror/lint';
    import { BehaviorSubject } from 'rxjs';
    
    const diagnostics$ = new BehaviorSubject<Diagnostic[]>([]);

    在实现 linter 时,除了直接返回 Diagnostic 外,还需要将结果写入 diagnostics$

    const invalidMetric = linter(async view => {
      const diagnostics: Diagnostic[] = await yourLinter(view);
      diagnostics$.next(diagnostics);
      return diagnostics;
    });

    接下来,我们在创建 Widget 时就可以订阅 Diagnostic 的变化来更新 UI 状态:

    class PlaceholderWidget extends WidgetType {
      constructor(string, private from: number, private to: number) {
        super();
      }
    
      toDOM(view: EditorView): HTMLElement {
        const span = document.createElement('span');
        span.innerHTML = renderYourWidget(view);
        // @ts-expect-error add destroy$ to span
        span.destory$ = diagnostics$.subscribe(diagnostics => {
          const error = diagnostics.find(d => d.from >= this.from && d.to <= this.to);
          if (error) {
            span.classList.add('cm-widget-error');
          } else {
            span.classList.remove('cm-widget-error');
          }
        });
        return span;
      }
    
      destroy(el: HTMLSpanElement) {
        // @ts-ignore
        if (el.destory$) {
          // @ts-ignore
          el.destory$.unsubscribe();
        }
      }
    }

    注意 PlaceholderWidget 的构造函数,我们需要在创建 Widget 的时候,传入 Widget 的插入位置(fromto)。

    const placeholderMatcher = new MatchDecorator({
      regexp: /#[0-9A-Za-z_]+/g,
      decorate: (add, from, to, match) => {
        const deco = Decoration.replace({
          widget: new PlaceholderWidget(from, to),
        });
        add(from, to, deco);
      },
    });
    const placeholders = ViewPlugin.fromClass(class {
      placeholders: DecorationSet;
    
      constructor(view: EditorView) {
        this.placeholders = placeholderMatcher.createDeco(view);
      }
    
      update(update: ViewUpdate) {
        this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders);
      }
    }, {
      decorations: instance => instance.placeholders,
      provide: plugin => EditorView.atomicRanges.of(view => {
        return view.plugin(plugin)?.placeholders || Decoration.none;
      }),
    });

    最终的效果如下:

  • 有多少「黄牛」在抢四川消费券?

    2024年9月,四川省财政厅投入了 3 亿资金用来向市民发放家装消费券

    在9月27日消费券被领取完后的 30 分钟,我在闲鱼上找到了跟四川家装消费券相关的87条搜索结果:

    补贴金额数量小计
    4k416k
    2k48k
    1.5k1319.5k
    0.9k1715.3k
    0.6k148.4k
    总计5267.2k

    在领取完后的一小时,相关搜索结果有 158 条。据此粗略估计,每天在闲鱼上被转卖的消费券大概价值 20 万元。

    根据官方的消费券发放安排,价值 3 亿的消费券会在 9 月 26 日到 10 月 31 日中的 31 天发放,假设能够全部发完,每天应该发放价值 967 万元的消费券。

    如果只有闲鱼上有黄牛,那么 9 月 27 日这天的黄牛含量大概在 2% (20/967≈2.07%)。

  • DRAFT: pre-bundle dependencies for vite

    背景

    如果我不想每次构建 SPA 的时,把项目的依赖也一并重新构建一遍,我应该怎么做?
    预打包项目依赖,当项目依赖长久没有发生变更时,不再重新 bundle node_modules。

    解决方案

    类似 Webpack DLL plugin,但是要更加智能。

    只预打包项目中多次使用的依赖包。
    同时支持 build/serve 两种模式。

    实现细节——预打包部分:

    用正则表达式找出项目中所有的 import 语句
    用单独的 vite config 配合 preBundle 插件为第三方包生成预打包文件

    实现细节——使用预打包文件:

    build 模式,这里涉及 rollup 相关的插件逻辑,需要让 rollup 将预打包的模块 id 解析为外部依赖,并且将预打包文件作为 prebuilt-chunk 输出。

    dev 模式,这里涉及 vite 跟 esbuild 相关的逻辑。
    由于我们并不会为所有的第三方依赖生成预打包文件,所以存在某些 optimizeDeps 模块引用预打包模块的情况。
    vite 会使用 esbuild optimize deps,所以需要配合 esbuild 插件处理。

    如何在导入方使用 commonjs 导出的 named export ?

    在预打包阶段, rollup 不会为 cjs entry 生成 named export,当我们在导入预打包文件时,需要对这些被转换成 esm 的 cjs 做一些额外处理。

    需要自己实现一个 transform hook:

    // input
    import { useState } from 'react';
    // ouput
    import React from 'react';
    const useState = React.useState;
    

    反思

    预打包多次使用的依赖会为每个依赖都创建一个 bundle 文件,这可能会让浏览器加载页面的时候需要发送更多的请求。

    但是考虑到这些预打包文件可能很少更新,浏览器本地缓存可以起到很好的效果。如果浏览器跟服务器之间使用的是 http2 协议,这些请求似乎也不太算是问题?
    如果要从根本上解决这个问题,还得靠 web bundle。

    实现 merged exports

    https://github.com/tc39/proposal-module-declarations?tab=readme-ov-file

    @ant-design/icons 中提供了很多 icons, 如果为每个 icon 都创建一个 prebundle chunk, 那么 output 目录中就会出现上千的小文件。

    要避免这种情况,需要实现合并 exports 的功能。

    // vite.config.mts
    
    plugins: [
      prebundleReference({
        merge: {
          '<ruleName>': ['<module-id-prefix>', '@ant-design/icons']
        }
      })
    ]
    

    以上的配置会在 transform 环节产生如下的代码:

    // pre-bundle-merged-<ruleName>.mjs
    import * as __at__ant_design_icons from '@ant-design/icons';
    import * as __at__ant_design_icons_Foo from '@ant-design/icons/Foo';
    export const __ns_at__ant_design_icons = __at__ant_design_icons;
    export const __ns_at__ant_design_icons_Foo = __at__ant_design_icons_Foo;
    

    对应的 manifest 部分

    [
      {
        "moduleId": "@ant-design/icons",
        "moduleFilePath": "pre-bundle-merged-<ruleName>.mjs",
        "exports": [
          "default",
          "__moduleExports"
        ],
        "exportAs": "__ns_at__ant_design_icons",
        "isCommonJS": false
      },
      {
        "moduleId": "@ant-design/icons/Foo",
        "moduleFilePath": "pre-bundle-merged-<ruleName>.mjs",
        "exports": [
          "default",
          "__moduleExports"
        ],
        "exportAs": "__ns_at__ant_design_icons_Foo",
        "isCommonJS": false
      },
    ]

    当在 reference 插件中使用时,需要转换 import 代码:

    // helper
    
    export function _prebundle_merge_get_default(moduleLike) {
      return moduleLike.default ?? moduleLike;
    }
    
    export function _prebundle_merge_get_named(moduleLike, name) {
      if(name in moduleLike){
        return moduleLike[name];
      }
      if(moduleLike.default && name in moduleLike.default) {
        return moduleLike.default[name];
      }
    }
    // default import
    import Foo from '@ant-design/icons/Foo';
    // =>
    import { __ns_at__ant_design_icons_Foo as __ns_at__ant_design_icons_Foo$1 } from '@ant-design/icons/Foo';
    const Foo = _prebundle_merge_get_default(__ns_at__ant_design_icons_Foo$1);
    
    
    // named import
    import { Foo } from '@ant-design/icons';
    // =>
    import { __ns_at__ant_design_icons_Foo as __ns_at__ant_design_icons_Foo$2 } from '@ant-design/icons/Foo';
    const Foo = _prebundle_merge_get_named(__ns_at__ant_design_icons_Foo$2, 'Foo');
    
    // ns import
    import * as Icons from '@ant-design/icons';
    // =>
    import { __ns_at__ant_design_icons_Foo as __ns_at__ant_design_icons_Foo$3 } from '@ant-design/icons/Foo';
    const Icons = __ns_at__ant_design_icons_Foo$3;

    prebundle merge transform 的逻辑应该在 commonjs 转换之后,因为 commonjs transform 假定 module 只会导出 { default: blabla }

    // input
    import {useCallback} from 'react';
    
    // transform commonjs
    import React from 'react';
    const useCallback = React.useCallback;
    
    // transform prebundle merge
    import { __ns_react as __ns_react$1 } from 'react';
    const React = _prebundle_merge_get_default(__ns_react$1);
    const useCallback = React.useCallback;

    好像也可以将 commonjs transform 跟 prebundle merge transform 合并?

    [
      {
        "moduleId": "react",
        "moduleFilePath": "pre-bundle-merged-<ruleName>.mjs",
        "exports": [
          "default"
        ],
        "exportAs": "default",
        "isCommonJS": true
      }
    ]
    import React, {useCallback} from 'react';
    
    // =>
    import { default as __ns_react$1 } from 'react';
    const React = _prebundle_merge_get_default(__ns_react$1);
    const useCallback = _prebundle_merge_get_named(__ns_react$1.'useCallback');

    接着,在 resolve 阶段,只需要将模块 id 都替换成 <path-to>/<moduleFilePath>

    预打包依赖项,真的可以提高构建效率吗?

    在我的项目中,第三方依赖的体积远大于实际的代码量,每次启动 dev server 或者构建 production artifacts,都需要消耗很多时间在转换 node_modules 代码上。预打包依赖就可以很好的解决这个问题。

    另外,配合 nx 的共享缓存,这个项目的所有贡献者、CI 流水线,都可以享受到预打包带来的提升。

    为啥不能用 UMD 格式提供 pre-bundle ?

    rollup 不支持为 umd bundle 生成 common shared chunk

  • 如何选购冰箱

    冰箱是柜子,冰柜是箱子

    影响冰箱摆放位置的产品参数

    作为家中体积最大的电器,找个合适的位置放冰箱很关键。影响冰箱摆放空间的参数有两个:

    • 散热方式
    • 开门方式

    两侧散热的冰箱需要预留更多的空间帮助全年无修的冰箱散热,通常需要在冰箱的左右两侧以及后侧各预留 10cm 左右的空间。

    底部散热或者顶部散热的冰箱对空间的需求比较小,适合摆放在比较狭小的空间。

    开门方式也会影响冰箱的摆放,虽然冰箱叫「箱」,但是开门方式跟柜子类似,主要使用抽屉跟平开门。所以在摆放冰箱的时候也应该像摆放柜子一样,考虑门板、抽屉跟其他家具的冲突。如果空间比较狭小,可以选择 90° 的平开门。

    另一个节省冰箱摆放空间的技巧是把冰箱插座的位置调整到冰箱的侧面,或者使用内嵌式插座。这样可以为冰箱节省 3 ~ 4cm 的深度空间。

    影响收纳能力的产品参数

    作为柜子,冰箱当然越能装越好,商家往往只会商品详情页上写明容量,例如,500L,但是我们往冰箱中存放的只会是固体。所以容量只能作为参考,真正影响使用体验的是冰箱的收纳方式,对应的产品参数是门款式。

    门款式通常对冷冻区的格局影响比较大,大家可以按照自己最常屯的物品选择适合的冷冻区格局。

    影响使用体验的产品参数

    串味是影响冰箱使用体验的一个常见问题,从冰箱的角度解决这个问题有两种方式:

    • 双循环风道
    • 净味系统

    现在多数冰箱采用风冷降温,冷空气会在冷藏区、冷冻区之间流动,只有单循环风道的冰箱会有跨区窜味的问题。一些冰箱通过引入双循环风道,将冷藏、冷冻区之间物理隔离开,从而避免串味的问题。

    净味系统则是引入了类似空气净化器的模块,也能在一定程度上减少冰箱的异味,异味浓度低了,串味的影响也会降低。

    影响保鲜功能的产品参数

    保鲜可以从两个层面理解 —— 防止腐败、防止风干

    为了防止腐败,我们应该尽量选择带有杀菌能力检测证书的冰箱 只声称可以杀菌但不提供证书的都看作没有杀菌能力,通过抑菌、灭菌来降低食物污染风险。

    为了防止冷风将新鲜蔬果吹干,可以选择带有湿度调节区域的冰箱,减少干冷空气对果蔬的影响。

    不过防止腐败跟防止风干是有些冲突的两个功能,潮湿的有氧环境有利于微生物繁殖,所以, 最好还是减少新鲜蔬果的存放时间,或者将蔬果单独分区存放。

    产品关键参数速查表

    • 摆放位置
      • 长宽高
      • 散热方式
      • 开门方式
      • 插座位置
    • 收纳能力
      • 容量
      • 门款式
    • 防串味
      • 是否双循环
      • 净味系统
    • 保鲜
      • 杀菌、抑菌能力证书
      • 果蔬保鲜区

  • 给 Email 写样式真的很难

    虽然 IE 退出了浏览器的历史舞台,但是 Windows 平台上的 Outlook 依然继承了 IE 的衣钵,继续在邮件客户端界发光发热。

    This ranks email clients based on their support among the 298 HTML and CSS features listed on Can I email.

    最好用内联样式

    很多情况下,使用外部样式表并不靠谱,比如 GMail 以及 Outlook 就会在特定情况下移除邮件 HTML 中的 style 标签,一个更保守的做法是使用元素上的内联样式。对于简单的通知邮件来说,完全够用了。

    如果嫌手搓样式很麻烦,可以先用 TailWind CSS 编写,然后让 ChatGPT 帮忙转换成内联样式。

    避免使用 base64 内联图片

    虽然 Base64 图片 URL 很简单易用,但是很多邮件客户端会把它们当作外部图片一样默认屏蔽。

    一个对展示效果更加友好的做法是通过 cid 引用内联图片,这时,图片会被作为邮件的附件一起发送给收件人,如果不希望正文图片被客户端识别为可下载的附件,则需要将附件的 Content-Disposition 设置为 inline,同时,还需要设置正确的 Content-Type

    Quick summary

    1. Inline only attachments: use multipart/related
    2. Non-inline only attachments: use multipart/mixed
    3. Inline and non-inline attachments use multipart/mixed and multipart/related
    mime – HTML-Email with inline attachments and non-inline attachments – Stack Overflow

    我的 Email 模板工作流

    考虑到 EMail HTML 本身的复杂性,为了简化 Email 模板的开发,我采用了下面的工作流:

    首先,使用 degit 初始化一个集成了 TailWind CSS 跟 Vite 的项目模板:

    npx degit kometolabs/vite-tailwind-nojs-starter email-template

    修改其中的 vite.config.mjs:

    import { resolve } from "path";
    import { defineConfig } from "vite";
    import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
    
    function optimizeSvgPlugin() {
      if (!!process.env.OPTIMIZE_IMAGES) {
        return ViteImageOptimizer({
          svgo: {
            plugins: [{ removeViewBox: false }, { removeDimensions: true }],
          },
        });
      }
    }
    
    export default defineConfig(() => ({
      build: {
        outDir: "../dist",
        emptyOutDir: true,
        // avoid inlining images
        assetsInlineLimit: () => false,
        rollupOptions: {
          input: {
            page1: resolve(__dirname, "src", "page1.html"),
            page2: resolve(__dirname, "src", "page2.html"),
          },
          output: {
            // avoid hash in assets filename
            assetFileNames: (assetInfo) => {
              const info = assetInfo.name.split(".");
              let extType = info[info.length - 1];
    
              // Explicitly handle image files
              if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
                extType = "img";
                // Return the original filename without hashing
                return `assets/${extType}/[name][extname]`;
              }
    
              // For other asset types, you can keep the default hashing behavior
              return `assets/${extType}/[name]-[hash][extname]`;
            },
          },
        },
      },
      root: "src",
      plugins: [optimizeSvgPlugin()],
    }));

    在编写 HTML 时需要手动创建一个引入 TailWind CSS 的 style 标签:

    <style>
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    </style>

    接下来就可以按照常规的方法用 HTML 画页面了,不过需要注意的是,大部分邮件客户端对 flex-box 的支持都比较差,在编写样式时需要注意。

    完成了 HTML 页面以及样式后,就可以借助 ChatGPT 将 TailWind 转换成内联样式的代码。最后,借助下面的脚本,即可将构建出来的 HTML 文件转换成 ejs 模板:

    // convert all `{{ foo }}` to `<%= foo %>`
    
    function convertToEjs(content: string) {
      return content.replace(/{{\s*(\w+?)\s*}}/g, "<%= $1 %>");
    }
    
    /**
     * Convert /assets/img/foo-bar.svg to cid:foo-bar
     * @param content
     */
    function toCidReference(content: string): string {
      return content.replace(/\/assets\/img\/([a-zA-Z0-9-)]+?)\.(svg|png|jpg|jpeg)/g, "cid:$1");
    }
    
    const files = await glob(dir + "/**/*.html");
    
    for (const filename of files) {
      const content = await fs.readFile(filename, "utf-8");
      const dirOfFile = path.dirname(filename);
      const newContent = toCidReference(convertToEjs(content));
      const newFilename = path.join(
        dirOfFile,
        path.basename(filename, ".html") + ".ejs",
      );
    
      await fs.writeFile(newFilename, newContent);
      echo(chalk.green(`Converted ${filename} to ${newFilename}`));
    }

    用来生成邮件模板的命令行操作如下:

    #!/usr/bin/bash
    OPTIMIZE_IMAGES=true yarn build
    cp ./dist/assets/img/* ./assets/img/
    npx tsx ./scripts/to-ejs-template.ts --dir ./dist

    参考链接

    How to Embed Images in Your Emails (CID, HTML Inline & More) | SendGrid

    mime – HTML-Email with inline attachments and non-inline attachments – Stack Overflow

  • 如何为博文自动生成可读的 Permalink

    WordPress 默认提供了一个比较适合英语文章的 Permalink 生成机制,如果你跟我一样使用中文标题,那自动生成的 Permalink 中就会包含汉字,这对 URL 而言并不友好。之前我都会手动给每篇博文翻译一个英文标题,再将其 slugify,做得多了就会觉得繁琐,于是我开发了一个 API,可以用来帮助自动化地完成这个过程。

    API 的调用方式如下,大家可以免费使用。

    GET https://blogkit.apps.gianthard.rocks/api/v1/slugify?t=如何科学饲养母猪
    
    HTTP/2 200 OK
    server: nginx
    date: Mon, 19 Aug 2024 10:01:06 GMT
    content-type: application/json; charset=utf-8
    age: 189318
    cache-control: public,max-age=31536000
    content-length: 24
    x-http2-stream-id: 3
    
    "scientific-sow-rearing"

    要在 WordPress 中自动调用的话,需要在 WordPress 管理页面添加一些代码:

    function suggest_slug_from_api( $slug, $post_id, $post_status, $post_type, $post_parent, $original_slug ) {
        // Only apply for new posts or when the slug is empty
        if ( empty($slug) ) {
            $post = get_post($post_id);
            $title = $post ? $post->post_title : '';
    
            if ( empty($title) ) {
                return $slug; // Return original slug if no title
            }
    
            $api_url = 'https://blogkit.apps.gianthard.rocks/api/v1/slugify';
    
            // Encode the title for use in URL
            $query = http_build_query(array('t' => $title));
            $url = $api_url . '?' . $query;
    
            // Make the API request
            $response = wp_remote_get($url);
    
            // Check if the request was successful
            if ( !is_wp_error($response) && wp_remote_retrieve_response_code($response) == 200 ) {
                $body = wp_remote_retrieve_body($response);
                $suggested_slug = json_decode($body, true);
    
                // If we got a valid slug, use it
                if ( $suggested_slug && is_string($suggested_slug) ) {
                    return $suggested_slug;
                }
            }
        }
    
        // If anything goes wrong or if the post already has a custom slug, return the original slug
        return $slug;
    }
    add_filter( 'wp_unique_post_slug', 'suggest_slug_from_api', 10, 6 );
    

    我使用的是 WP Code 插件,用这个插件管理 PHP 代码片段非常方便,免费版也很够用。

    https://wordpress.org/plugins/insert-headers-and-footers/
    注意只在 Admin 页面上启用这个片段

    上面的代码工作完成后,WordPress 就会在草稿保存后自动生成 URL slug 。需要注意的是,编辑页面的前端可能不会及时更新 URL slug 预览,但是发布后就会使用通过 API 生成的 URL Slug。

  • 前端处理响应式图片

    MDN: Responsive images 介绍了在前端实现响应式图片的几种方法:

    • 为不同尺寸的屏幕加载不同大小的图片(srcset 配合 sizes
    • 固定图片大小,为不同 DPI 的屏幕加载不同 DPI 的图片(srcset
    • 为不同尺寸的屏幕加载不同的图片(picture 配合多个 source

    WordPress 采用 srcset/sizes 方案实现响应式图片,默认的设置为 sizes="(max-width: {{image-width}}px) 100vw, {{image-width}}px"。当页面宽度小于图片原始宽度时,以页面宽度(100vw)作为图片的显示宽度,当页面宽度大于图片宽度时,总会以图片原始宽度作为显示宽度。但实际上,不是所有的图片都会以原始宽度在页面上展示,由于排版样式的影响,图片的展示宽度往往跟原始宽度不一致。

    为了解决这个问题,需要使用 Enhanced Responsive Images 插件,这个插件会基于当前主题的 Content Width, Wide Width 为图片元素设置 srcset/sizes,相较于默认的实现,可以为大屏设备在图片宽度小于窗口宽度时提供更加合适的缩略图尺寸。

    Performance Lab 提供的 Enhanced Responsive Images 插件

    启用后的效果如下图:

    可以看到,虽然 img 元素的大小只有 800×312 px,但是浏览器选择了展示原图(1341 x 523 px),这是因为我的屏幕启用了 HiDPI,1 css px 约等于 2 显示器 px,这种情况下,浏览器使用更大尺寸的图片是合理的。

  • 使用 WebP/AVIF 压缩博文图片

    使用 WebP/AVIF 压缩博文图片

    往文章中贴截图的时候,如果使用压缩率较低的图片格式(jpeg/png),长时间的图片下载耗时会降低用户的阅读体验。

    根据 CanIUse 的统计,目前绝大多数的网上冲浪用户使用的浏览器已经支持了 WebP/AVIF 这类高效的图片格式。

    https://caniuse.com/avif Can I use AVIF?
    https://caniuse.com/webp Can I use WebP?

    以文章开头的那张图片为例,原图大概 108KB,转换成 AVIF 格式并压缩后,体积缩小了接近 90% 。

    如果想要给 WordPress 启用自动转换的功能,我推荐使用下面的免费插件,只要你的 PHP 支持 imagick/GD 插件,就可以使用。

    https://wordpress.org/plugins/performance-lab/

    安装完成后需要在管理面板中启用 Mordern ImageFormats 功能。

    如果需要把已有的图片都转换成 WebP/AVIF,可以使用下面的插件批量转换。

    https://wordpress.org/plugins/compressx/

    安装完成后就可以在后台管理页面批量转换格式或者单独地转换格式。

    一些疑问

    如果用户的浏览器不支持 WebP/AVIF 该怎么办?

    AVIF 在比较新的浏览器中普及率很高 只有现代 IE —— Safari 不支持,WebP 的兼容性也非常好,不过万一有人用史前时代的浏览器上网呢?

    其实这个问题完全不用担心,上面介绍的插件在生成 WebP/AVIF 格式后并不会删除原图,浏览器会跟 WordPress 协商应该使用何种格式的图片向用户展示。

    怎么确认我正在使用 WebP/AVIF 版本的图片?

    仅仅依靠文件名无法区分图片格式,应该检查浏览器请求的响应部分,如果 Content-Type 的值为 image/avif 说明这张图片正在以 AVIF 格式展示,WebP 也是同理。

    为什么图片看起来有些糊?

    CompressX 插件默认使用有损压缩,建议手动调整为无损压缩选项。

    为什么转换格式这么慢?

    转换速度完全取决于你的服务器配置,简而言之,加钱世界可得~

    为什么 PNG 图片没有被自动转换成 WebP/AVIF ?

    因为 Modern Image Format 暂时还没加上这个功能,具体可以看这里的讨论:https://github.com/WordPress/performance/pull/1421。就目前而言,可以使用 CompressX 插件手动转换 PNG 图片。

  • 不要钱的博客网站性能优化

    赛博菩萨 Cloudflare 给免费帐户提供了 Cache Rules 功能,除了可以点亮域名后的云朵图标之外,还可以利用 Cache Rules 缓存博客网站的页面,提高访问者的使用体验。

    要给 WordPress 网站启用 Cache Rules 推荐使用下面这个插件:

    https://wordpress.org/plugins/wp-cloudflare-page-cache/#developers
    Super Page Cache for Cloudflare

    不过在使用这个插件的时候需要注意,这个插件自带的 Cloudflare API Token 权限说明有误,请参考下面的权限列表。

    如果你想我一样还使用了 LiteSpeed Cache 等缓存插件,建议参考 Super Page Cache 的 FAQ 来调整其他关联插件的配置。

    完成插件的安装跟配置后,可以在 Cloudflare 控制台看到由插件创建的 Cache Rule:

    至此,你的网站就可以享受到免费的全球 CDN 加成了。

  • 等一个后续

    像个鸵鸟一样,把头埋进沙子里。

    2024-07-09 五问煤制油罐车混装食用油:流向哪里?有何危害?谁该负责?

    https://web.archive.org/web/20240809024444/https://www.thepaper.cn/newsDetail_forward_28010047

    2024-08-27 国务院食安办通报“罐车运输食用植物油乱象问题”调查处置情况(简讯)

    https://web.archive.org/web/20240829090033/http://paper.people.com.cn/jksb/html/2024-08/27/content_26077451.htm

    2024-08-27 关于征求《食用植物油散装运输卫生要求》拟立项强制性国家标准项目意见的通知

    https://web.archive.org/web/20240829090629/https://www.samr.gov.cn/bzjss/zqyj/art/2024/art_94a628a1632644a2862d4f2ba8f6fc43.html

    2024-08-08 披露“75人涉嫌盗尸4000具”案律师:我甘愿受处罚,希望推进法律完善

    https://web.archive.org/web/20240809024851/https://news.sina.com.cn/s/2024-08-08/doc-inchxvay1591238.shtml