← 返回蜂巢洞察

如何在 Cloudflare Pages 上使用 Astro SSR 构建一个无头 WordPress 前端界面

本教程将向您展示如何将WordPress作为无头内容管理系统来使用,并将其前端框 … Read More

Socrates

本教程将向您展示如何将WordPress作为无头内容管理系统来使用,并将其前端框架Astro部署到Cloudflare Pages上。

在我最近参与的一个项目中,需求是将WordPress作为该网站的后端。内容管理、博客文章以及媒体文件都通过WordPress的管理界面进行操作。而前端部分则可以是自定义的主题、模板,也可以是通过Elementor工具制作的定制页面。

虽然我也可以使用Elementor来实现相同的功能,但这样的开发过程会更为繁琐,且后期维护也会更加困难。当设计变得较为复杂时,拖放编辑功能就会变得不再适用,每进行一次微调都会耗费大量的时间。

作为一名全栈开发者,对我来说,直接编写代码反而会更加高效,而且生成的代码质量也会更高。像Claude Code这样的工具还能进一步优化迭代流程。因此,我依然坚持使用WordPress作为后端,并决定通过编程方式来构建前端部分。

我想分享一下我是如何实现这一目标的,这样如果您也面临类似的需求,就能知道该从哪里入手。

完成本教程后,您将能够:

  • 通过一个子域名,利用WordPress的REST API来提供内容服务

  • 使用Astro前端框架,在主域名上渲染内容

  • 每次进行git推送时,都能自动触发Cloudflare Pages的部署流程

  • 为这种无头WordPress架构做好安全防护措施

  • 确保在两个系统中都能正常预览文章草稿

先决条件:您需要熟悉命令行操作,对WordPress的管理界面有基本的了解,并且具备足够的JavaScript基础知识,能够阅读和编写简单的函数。

为了顺利完成本教程,您需要准备一个已安装的WordPress站点、一个GitHub账户以及一个Cloudflare账户。

目录

为什么选择无头WordPress?

无头WordPress将内容管理与内容呈现分离开来。WordPress继续承担它擅长的任务:存储内容并为编辑人员提供熟悉的管理员界面,而专门的前端框架则负责内容的渲染、路由处理及性能优化。

在以下情况下,这种分离模式会带来显著的优势:

  • 如果您的内容团队已经熟悉WordPress,将他们转移到其他系统只会影响工作效率。无头WordPress能够保持他们现有的工作流程,同时为您提供现代化的前端界面。

  • 当您的网站需要某种设计或交互方式,而普通的WordPress主题或页面构建工具无法实现时,定制化的仪表板、交互式工具、数据驱动的布局,或是与非WordPress API的集成方案,都是不错的选择。

  • 如果您希望获得快速的内容呈现效果以及现代化的开发工具,同时又不想从头开始重建内容管理系统,那么无头WordPress正是您的理想选择。WordPress在处理内容和媒体文件方面表现优异,而基于CDN的JavaScript前端框架也能高效地完成内容分发工作——这种分离架构能让双方各司其职,发挥各自的优势。

  • 如果您需要在多个平台上使用相同的内容,那么一个WordPress实例就可以通过同一个REST API同时为营销网站、移动应用以及内部管理界面提供内容支持。

然而,并非所有网站都适合采用无头WordPress架构。如果您的网站只是一个简单的宣传资料,或者只由一个人负责所有的管理员工作,又或者您没有足够的开发资源来维护第二个代码库,那么普通的WordPress主题可能才是更合适的选择。

架构原理

“无头”这一概念意味着要剥夺WordPress的前端处理功能。在这种情况下,WordPress不再负责生成并向用户发送HTML页面,而仅通过REST API来存储和提供内容;真正负责呈现给用户的界面,则由专门的前端框架来完成。

无头WordPress架构图:Cloudflare Pages与Astro结合使用,通过WordPress REST API获取内容,GitHub实现自动部署,编辑人员则通过CMS子域名进行操作。

当用户访问网站时,请求会首先到达Cloudflare Pages,然后由Astro服务器处理。Astro会通过WordPress的REST API获取所需内容,生成HTML页面后返回给用户。整个过程中,WordPress从未直接接触用户的浏览器。

内容编辑人员仍然可以通过CMS子域名登录WordPress管理员界面,像平常一样进行内容的编写、发布和管理。一旦内容被发布,就会立即生效——因为Astro会在每次请求时都获取最新的数据,所以根本不需要重新构建页面。

从WordPress 4.7版本开始,REST API就已经成为其内置功能之一。因此,您无需使用GraphQL插件、付费的无头CMS服务,也不需要额外的基础设施支持。

为什么选择Astro?

在这里,您也可以使用Next.js、Nuxt或SvelteKit等框架。但我之所以选择Astro,是因为它的默认配置非常适合这种使用场景。

Astro会将组件编译成普通的HTML代码,并且默认情况下不会向浏览器发送任何JavaScript代码。只有在你确实需要使用客户端JavaScript代码的地方,才应该添加它们。

对于那些由内容管理系统驱动的网站来说,大多数页面其实并不需要使用客户端JavaScript。采用服务器端渲染模式意味着每次请求都会在运行时从WordPress系统中获取最新的数据,因此当内容发生变化时,这些变化会立即生效,而无需重新构建整个网站。Cloudflare提供了官方适配器来处理编译后的输出文件;而Tailwind v4则可以通过Vite插件进行集成,而且完全不需要配置文件。

如果WordPress不是必须使用的工具,我就会选择使用Next.js搭配Payload CMS。Payload是一种基于TypeScript开发的、功能完备的内容管理系统,它可以与Next.js项目一起使用,从一开始就能让你对内容结构有更强的控制能力。但由于我们的需求是必须使用WordPress,而对于那些需要通过WordPress的REST API来构建前端界面的项目来说,Astro无疑是一个更快、更简洁的选择。

基础设施配置

我的配置方案如下:域名是在Namecheap注册的,网站托管在Hostinger提供的共享主机上,同时我也使用Google Workspace作为电子邮件服务。以下步骤适用于任何类型的服务器环境,无论是使用cPanel或hPanel管理的共享主机,还是安装了Apache或Nginx的VPS,甚至是自己管理的服务器。

步骤1:将DNS记录迁移到Cloudflare

首先,你需要将你的域名的名称服务器迁移到Cloudflare。这样不仅可以获得免费的DDoS防护和SSL加密服务,还可以将自定义域名与Cloudflare Pages关联起来。

在迁移之前,请确保所有的DNS记录都已经被正确转移,包括网站的A记录或CNAME记录。对于电子邮件服务来说,你需要从你的邮件服务提供商的管理面板中获取MX、SPF、DKIM和DMARC等设置信息,并先将这些信息添加到Cloudflare的DNS系统中;否则,在DNS记录传播完成之前,你的电子邮件服务可能会出现故障。

步骤2:创建内容管理系统的子域名

你需要将WordPress网站迁移到cms.yourdomain.com这个子域名上,这样主域名就可以被Astro使用了。在Cloudflare的DNS设置中,添加一条A记录,将cms指向你的服务器IP地址;如果你的托管服务使用的是CDN服务,也可以使用CNAME记录。然后,在你的主机管理面板中创建这个子域名,并将其指向相同的WordPress目录。

有一点需要注意:你的服务器需要拥有自己的SSL证书,这样才能确保Cloudflare与你的网站服务器之间的连接能够正常工作。虽然Cloudflare会在其边缘节点处理SSL加密任务,但如果你的网站服务器没有SSL证书,就会出现525错误。

在Hostinger平台上,新创建的子域名并不会自动配置SSL证书。你需要通过hPanel手动安装SSL证书;在cPanel平台上,可以使用Let’s Encrypt服务;而在VPS上,则可以使用Certbot工具来生成SSL证书。

将WordPress网站从主域名上迁移到子域名上,也会导致你的主域名的/wp-admin路径不再存在,这样可以降低安全风险。不过,在子域名上,默认的登录路径仍然是/wp-admin。这是你需要首先进行修改的地方——关于这个问题的更多细节,请参见文末的“注意事项”部分。

WordPress配置设置

告知WordPress网站实际位于子域名上

wp-config.php文件中,在“配置完成,无需再编辑!”这条注释之前:

define('WP_HOME', 'https://cms.yourdomain.com');  
define('WP_SITEURL', 'https://cms.yourdomain.com');  

WordPress管理界面现在的地址是cms.yourdomain.com/wp-admin。使用根域名的旧路径已经不再有效,这是有意为之的。

必须使用的插件:Redirect and Preview

WordPress在wp-content目录下有一个名为mu-plugins的文件夹。放在这个文件夹中的插件会被视为必须使用的插件。它们会在每次请求时自动加载,优先于其他普通插件,并且无法通过管理界面来激活或禁用它们。因此,将这些插件放在这里正是为了确保某些功能不会被意外关闭。

创建文件wp-content/mu-plugins/headless-redirect.php

<?php  
/*
插件名称:Headless Redirect  
描述:将前端访问者重定向到Astro网站,并修改WordPress的预览链接设置。  
*/

add_action('template_redirect', function() {
    if (is_user_logged_in()) return;
    if ($_SERVER['HTTP_HOST'] === 'cms.yourdomain.com') {
        wp_redirect('https://yourdomain.com', 302);
        exit;
    }
});

add_filter('preview_post_link', function(\(link, \)post) {
    $token = HEADLESS-preview_SECRET;
    \(type  = \)post->post_type;
    return 'https://yourdomain.com/preview?type=' . \(type . '&id=' . \)post->ID . '&token=' . $token;
}, 10, 2);

当WordPress准备渲染页面时,template_redirect插件会被执行。如果访问者未登录且请求来自CMS子域名,系统会将其重定向到主前端页面;已登录的编辑人员则可以正常进入管理界面。而发送到/wp-json/...的REST API请求不会经过template_redirect处理,因此不受其影响。

preview_post_link插件会改变编辑人员在预览文章时所看到的页面内容。默认情况下,WordPress会使用自己的主题进行预览,但在无头架构中,这个主题会导致页面显示为空白。
这个插件会将预览链接替换为指向您Astro平台/preview页面的请求,同时传递文章ID、文章类型以及一个秘钥。Astro平台的预览页面会利用这些信息通过REST API获取文章内容,并将其以与在线版本完全相同的方式呈现出来。

清理插件

现在该删除所有用于渲染前端的插件了:页面构建工具、缓存插件以及用于配置服务器环境的插件等。

不过,您应该保留Akismet、Wordfence和Yoast SEO插件。Yoast会直接在REST API响应中添加SEO元数据和Open Graph信息,而这些数据会被Astro平台的post.yoast_head_json字段读取。

接下来,将当前激活的主题切换为一个轻量级的默认主题。虽然WordPress要求至少有一个激活主题,但实际上没有人会看到这个默认主题。

Astro前端

首先使用pnpm create astro@latest命令创建项目,然后安装Cloudflare适配器和Tailwind插件:

pnpm add @astrojs/cloudflare
pnpm add -D @tailwindcss/vite tailwindcss

astro.config.mjs

import { defineConfig } from 'astro/config'
import cloudflare from '@astrojs/cloudflare'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  output: 'server',
  adapter: cloudflare({ imageService: 'passthrough' }),
  vite: { plugins: [tailwindcss()] },
})

output: 'server' 这个设置会使 Astro 进入完全的 SSR 模式。如果不使用这个选项,Astro 会在构建时预先渲染页面,而这会导致那些依赖于在构建时还不存在的 WordPress 内容的动态路由(比如 /blog/[slug])无法正常工作。

imageService: 'passthrough' 这个设置对于使用 Cloudflare Workers 是必不可少的。Astro 的默认图像处理服务依赖于 Sharp,而 Sharp 又需要用到 child_processfs 这些 Node.js 内置模块。但在 Cloudflare Workers 环境中,这些模块并不存在,因此会导致部署失败。将 imageService 设置为 ‘passthrough’ 可以完全跳过图像处理步骤,直接使用标准的 标签来显示图片。

.env

WORDPRESS_API_URL=https://cms.yourdomain.com

在部署之前,请将这个变量添加到 Cloudflare Pages 项目设置中的 “环境变量” 部分。

src/lib/wordpress.js

这个文件是所有 WordPress API 请求的集中处理点。将所有的 API 请求集中在这个文件中,意味着如果 API 地址或认证信息发生变化,只需要修改这一个文件即可。

_embed 这个参数非常重要。默认情况下,帖子的响应内容只包含帖子本身的数据,而推荐图片、作者信息以及分类信息则是单独存在的实体,它们各自都有独立的 ID。如果没有 _embed 这个参数,就需要通过额外的 API 请求来获取这些信息。而添加了这个参数之后,所有相关的信息都会被一起包含在同一个响应结果中。

cache: 'no-store' 这个设置在每次进行 API 请求时都是必须使用的。Cloudflare Workers 内部会使用一个独立的缓存系统来存储请求结果,这个缓存系统与 HTTP 中的 Cache-Control 头信息是分开管理的。如果不禁用这个缓存功能,Cloudflare 会在边缘服务器上缓存你的 WordPress API 响应结果。因此,当编辑者发布一篇新帖子时,由于系统中仍然保存着旧版本的帖子内容,前端页面可能会显示旧版本的信息。

const WP_URL = import.meta.env.WORDPRESS_API_URL

const fetchWP = (path) =>
  fetch(`\({WP_URL}\){path}`, { cache: 'no-store' }).then((r) => r.json())

export const getPosts = (page = 1, perPage = 10) =>
  fetchWP(`/wp-json/wp/v2/posts?_embed&per_page=\({perPage}&page=\){page}`)

export const getPostBySlug = async (slug) => {
  const posts = await fetchWP `/wp-json/wp/v2/posts?_embed&slug=${slug)`
  return posts[0]
}

export const getCategories = () =>
  fetchWP `/wp-json/wp/v2/categories`

export const getPostsByCategory = (categoryId, page = 1) =>
  fetchWP `/wp-json/wp/v2/posts?_embed&categories=\({categoryId}&page=\){page}`)

export const getAllPostsForSitemap = () =>
  fetchWP `/wp-json/wp/v2/posts?_fields=slug,modified&per_page=100`

站点地图功能使用_fields而非_embed来获取所需的字段,从而确保请求的体积保持较小。

src/middleware.js

中间件会在每个请求被处理之前被执行。这个中间件会为所有的服务器端渲染响应添加Cache-Control: no-store头信息,这样Cloudflare就不会缓存这些已渲染的HTML页面。

export function onRequest(_context, next) {
  return next().then(response => {
    const newResponse = new Response(response.body, response)
    newResponse.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate')
    newResponse.headers.set('CDN-Cache-Control', 'no-store')
    return newResponse
  })
}

Astro提供的原始Response对象的头部信息是不可变的,因此不能直接使用.headers.set()来修改这些头部信息。解决办法是使用原始的响应体作为参数来创建一个新的Response对象,因为新对象的头部信息是可以被修改的,所以此时就可以使用.set()方法了。CDN-Cache-Control是Cloudflare专门设置的一个头部信息,它用于控制边缘节点上的缓存行为,这个头信息的设置与标准的Cache-Control头信息是相互独立的。

src/layouts/Layout.astro

每个页面都会经过这个布局结构。HTML结构、元标签以及全局导入的内容都放在这里,这样就不需要在每个页面上都重复编写这些内容了。

---
interface Props {
  title: string
  description?: string
}
const { title, description = '' } = Astro.props
---
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    {title}</title>
    <meta name="description" content={description} />
  </head>
  <body>
    <slot name="nav" />
    <main id="main-content"><slot /&gt></main>
    <slot name="footer" /&gt>
  </body>
</html>
</code></pre>
<p>使用命名插槽可以让导航栏和页脚位于<code><main></code>标签之外,这样就能保证HTML结构的正确性,从而提升页面的可访问性。</p>
<h3 id="heading-srcpagesblogindexastro">src/pages/blog/index.astro</h3>
<pre><code class="language-astro">---
import Layout from '../../layouts/Layout.astro'
import { getPosts, getCategories, getPostsByCategory } from('../../lib/wordpress'

const page = Number(Astro.url.searchParams.get('page') ?? 1)
const categoryId = Astro.url.searchParams.get('category')

const [posts, categories] = await Promise.all([
 categoryId ? getPostsByCategory(categoryId, page) : getPosts(page, 10),
  getCategories(),
])
---
<Layout title="Blog">
  <nav>
    <a href="/blog"&gt>全部文章</a>
    {categories.map((cat) => (
      <a href={`/blog?category=${cat.id}`}>">{cat.name}</a>>
    ))}
  </nav>

  <ul>
    {posts.map((post) => {
      const image = post._embedded?.['wp:featuredmedia']?.[0]?.source_url
      const imageAlt = post._embedded?.['wp:featuredmedia']?.[0)?.alt_text ?? ''
      return (
        <li>
          <img src={image} alt={imageAlt} />
          <a href `{`/blog/${postslug}`}` set:html={post.title.rendered} />>
          <div set:html={post.excerpt.rendered} />
        </li>
      )
    ))}
  </ul>

  {page > 1 && <a href={`/blog?page=${page - 1}`}>上一页</a>}
  <a href={`/blog/page=${page + 1}`}>下一页</a>
</Layout>
</code></pre>
<p><code>Promise.all</code>会并行获取帖子和分类信息。分类筛选功能是通过URL查询字符串来实现的,因此同一个页面既能够处理<code>/blog</code>请求,也能处理<code>/blog?category=5</code>请求,而无需为这两种请求设置不同的路由路径。</p>
<p>特色图片存储在<code>post._embedded['wp:featuredmedia'][0]</code>中,因为<code>_embed</code>会将媒体对象直接嵌入到帖子响应数据中。</p>
<h3 id="heading-srcpagesblogslugastro">src/pages/blog/[slug].astro</h3>
<pre><code class="language-astro">---
import Layout from '../../layouts/Layout.astro'
import { getPostBySlug } from!!)
const { slug } = Astro.params
const post = await getPostBy Slug(slug)
if (!post) return Astro.redirect('/404')

const image    = post._embedded?.['wp:featuredmedia']?.[0]?.source_url
const imageAlt = post._embedded?.['wp:featuredmedia']?.[0)?.alt_text ?? ''
const author   = post._embedded?.author?.[0]?.name
const seoTitle = post.yoast_head_json?.title ?? post.title.rendered
const seoDesc  = post.yoast_head_json?.og_description ?? ''
---
<Layout title={seoTitle} description={seoDesc}>
  <article>
    < h1 set:html={post.title.rendered} />
    <>p>{author} · {new Date(post.date).toLocaleDateString()}</p>
    {image && <img src={image} alt={imageAlt} />}
    <div set:html={post.content.rendered} />>
  </article>
</Layout>
</code></pre>
<p>对于WordPress内容,应该使用<code>set:html</code>来渲染页面,而不是<code>{post.content.rendered}</code>。因为Astro会将大括号内的代码视为普通文本,并会对其中的HTML标签进行转义处理,这样一来,页面上显示的就会是未经过渲染的原始标签代码。</p>
<p>务必添加<code>if (!post) return Astro.redirect('/404')</code>这一判断语句。如果有人访问了一个不存在的帖子链接,API会返回一个空数组;如果没有这个保护机制,尝试访问<code>undefined</code>对象的属性时会引发错误,从而导致Cloudflare Worker程序崩溃,并返回500错误代码。</p>
<p>只有当Yoast SEO功能处于激活状态时,<code>post.yoast_head_json</code>才可用。其中包含了Yoast生成的SEO标题和描述信息。使用这个数据,就可以让在WordPress中做的SEO优化工作自动应用到Astro前端界面中。</p>
<h3 id="heading-srcpagessitemapxmlts">src/pages/sitemap.xml.ts</h3>
<pre><code class="language-ts">import type { APIRoute } from 'astro'
import { getAllPostsForSitemap } from '../lib/wordpress'

export const GET: APIRoute = async () => {
  const posts = await getAllPostsForSitemap()

  const urls = [
    { loc: 'https://yourdomain.com/', lastmod: new Date().toISOString() },
    { loc: 'https://yourdomain.com/blog/', lastmod: new Date().ISOString() },
    ...posts.map((p) => ({
      loc: `https://yourdomain.com/blog/${pslug}/`,
      lastmod: p.modified,
    }),
  ]

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
\({urls.map((u) => `  <url>\n    <loc>\){u.loc}</loc>\n    <>lastmod>${u.lastmod}</lastmod>\n  </url>>`).join('\n')]
</urlset>`

  return new Response(xml, { headers: { 'Content-Type': 'application/xml' } })
}
</code></pre>
<p>这种架构会在每次请求时生成新的XML文件,因此站点地图始终能够反映当前已发布的文章内容,而无需进行重新构建。</p>
<h3 id="heading-srcstylesglobalcss">src/styles/global.css</h3>
<pre><code class="language-css">@import "tailwindcss";

@theme {
  --color-brand: #your-color;
  --font-sans: 'Your Font', sans-serif;
}
</code></pre>
<p>Tailwind v4通过<code>@theme</code>块采用了“以CSS为主”的配置方式。在这里定义的CSS变量会自动成为Tailwind框架中的实用功能。<code>--color-brand</code>会被转换为<code>bg-brand</code>、<code>text-brand</code>等格式。因此不需要使用<code>tailwind.config.js</code>文件。</p>
<h2 id="heading-cicd-with-cloudflare-pages">使用Cloudflare Pages实现CI/CD流程</h2>
<p>在完成了Astro代码的配置之后,最后一步就是将其部署到实际环境中。Cloudflare Pages可以直接与GitHub连接,因此无需维护额外的部署管道。</p>
<p>具体操作步骤如下:</p>
<ol>
<li>
<p>将你的仓库推送到GitHub上。</p>
</li>
<li>
<p>登录Cloudflare Pages网站,创建一个新的项目,并将其与你的GitHub仓库关联起来。</p>
</li>
<li>
<p>将构建命令设置为<code>pnpm build</code>,并将输出目录设置为<code>dist</code>。</p>
</li>
<li>
<p>在“环境变量”设置中,添加<code>WORDPRESS_API_URL</code>,并将其指向<code>https://cms.yourdomain.com</code>。</p>
</li>
<li>
<p>最后进行部署操作。</p>
</li>
</ol>
<p>在完成首次部署后,每次向<code>main</code>分支推送代码都会自动触发新的部署流程。Cloudflare会负责执行构建任务,几分钟内新版本就会在全球范围内生效。由于Astro会在每次请求时从WordPress系统中获取内容,因此WordPress中的更新也会立即反映出来。开发者推送代码与编辑人员发布文章是两个完全独立的操作过程。</p>
<h2 id="heading-final-thoughts">最后的思考</h2>
<p>采用这种架构的原因是内容团队已经习惯了使用WordPress,而且也没有必要改变这一现状。</p>
<p>如果你是从零开始搭建网站,并且还没有选择任何内容管理系统,那么这种架构可能并不适合你。在这种情况下,建议选择Next.js或Payload CMS这样的工具,因为这些系统的后端和前端从设计之初就就是为了协同工作而开发的。</p>
<p>但是,如果你的团队已经熟悉使用WordPress,而且你需要一个页面构建工具无法实现的定制化前端界面,那么这种分离式的架构确实是有意义的。</p>
<p>优点:</p>
<ul>
<li>
<p>内容编辑人员可以继续使用他们熟悉的WordPress系统,无需进行任何重新培训或数据迁移操作。</p>
</li>
<li>
<p>前端可以对设计和功能进行完全的控制,不会受到任何主题或插件的限制。</p>
</li>
<li>
<p>每次推送代码都会自动触发部署,内容更新会立即生效,而无需进行重新构建。</p>
</li>
<li>
<p>对于大多数网站来说,这种架构并不会增加额外的成本。WordPress可以继续使用现有的托管服务;Cloudflare Pages在常规使用范围内是免费的,如果需求量超过免费限制,每月只需支付5美元即可使用其高级功能。</p>
</li>
</ul>
<p>缺点:</p>
<ul>
<li>
<p>需要维护两个系统而不是一个。你需要分别管理WordPress的安装和更新、插件配置以及Astro代码库。</p>
</li>
<li>
<p>WordPress的REST API存在一些局限性。对于复杂的内容结构或实时功能来说,使用它进行处理需要额外的开发工作,相比之下,专为内容管理系统设计的解决方案会更加高效。</p>
</li>
<li>
<p>适配器和部署目标是紧密关联的。@astrojs/cloudflare v13版本不再支持Cloudflare Pages,因此如果继续使用Pages,就必须停留在v12版本。具体细节请参阅“注意事项”部分。</p>
</li>
<li>
<p>进行前端代码修改时需要依赖开发人员。而使用Elementor这样的工具,任何具有管理员权限的人都可以直接在浏览器中调整布局。但在这种架构下,任何与内容无关的视觉效果更改都需要通过编写代码来实现,这意味着这些更改必须由开发人员来完成。</p>
</li>
</ul>
<p>当前的系统架构是:在现有的托管环境中使用WordPress,而在Cloudflare Pages上使用Astro;同时,GitHub被用作开发环境和生产环境之间的桥梁。这种配置能够有效地解决某个特定问题,但对于其他情况来说,还有更好的选择。</p>
<h2 id="heading-good-to-know">需要了解的事项</h2>
<p><strong>立即更改默认的登录URL。</strong>所有恶意程序都会攻击<code>/wp-login.php</code>和<code>/wp-admin</code>这两个路径。请安装WPS Hide Login插件,并将默认登录地址修改为自定义地址,这样当有人尝试访问这些路径时,系统会返回404错误页面。</p>
<p><strong>删除</strong><code>/wp-json/wp/v2/users</code>这个端点。该端点会公开显示所有用户的用户名列表。在无头模式下,你可以通过<code>_embed</code>获取作者的相关信息,因此这个端点是多余的。你可以在mu-plugin中添加以下代码来删除它:</p>
<pre><code class="language-php">add_filter('rest_endpoints', function($endpoints) {
    unset($endpoints['/wp/v2/users']);
    unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']);
    return $endpoints;
});
</code></pre>
<p><strong>禁用XML-RPC功能并启用两步验证。</strong>在mu-plugin中添加<code>add_filter('xmlrpcenabled', '__return_false')</code>这一行代码——因为在无头模式下你并不需要使用XML-RPC功能,而且这个功能很容易成为暴力攻击的目标。同时,请开启Wordfence的暴力防护功能,并为所有管理员账户启用WP 2FA两步验证。</p>
<p><strong>如果你是通过Cloudflare Pages的git-push CI流程进行部署的,那么请<strong>不要</strong>将<code>@astrojs/cloudflare</code>升级到v13版本。</strong>v12版本生成的<code>dist/_worker.js</code>文件格式是Cloudflare Pages的CI系统所期望的;而v13版本生成的文件格式不同,导致CI系统在尝试使用<code>wrangler deploy</code>命令进行部署时会出现问题,最终会显示404错误页面,且不会提供任何有用的错误信息。</p>
<p><strong>v12版本的适配器会在<code>entrypointResolution</code>这个参数上抛出弃用警告。</strong>你可以通过在适配器配置中添加<code>entrypointResolution: 'auto'</code>这一行代码来忽略这个警告。但在提交代码之前,请务必先进行测试,因为这个设置会改变构建系统查找Worker入口文件的方式。</p>
<p><strong>自定义文章类型也需要遵循相同的配置规则。</strong>你需要使用<code>show_in_rest: true</code>和<code>rest_base</code>这两个参数来注册这些自定义文章类型,这样它们就会出现在<code>/wp-json/wp/v2/your-base</code>这个路径下。同样的数据获取辅助函数、<code>_embed</code>以及slug路由规则,对这些自定义文章类型也同样适用。</p>
<p><strong>REST API会返回分页相关的头部信息。</code>在调用<code>.json()</code>方法之前,原始响应中已经包含了<code>X-WP-Total</code>和<code>X-WP-TotalPages</code>这两个头部字段。如果你想要实现正确的前后页分页功能,就应该直接使用这些头部信息,而不是自己去判断下一页是否存在。</p>
<p><strong>在调用API时,请使用try/catch语句进行异常处理。</strong>如果WordPress服务器无法被访问,普通的请求操作就会抛出错误并返回500状态码;而使用try/catch语句进行处理的话,系统会返回一个空页面,这样处理错误的方式要合理得多。</p>
<p><strong>预览功能需要使用应用密码。</strong>WordPress 5.6版本在“用户”→“个人资料”设置中增加了应用密码功能。因此,在你的<code>.env</code>文件中,<code>WP_APP_USER</code>和<code>WP_APP_PASSWORD</code>这两个变量的值应该设置为应用密码,而不是普通的管理员密码。请为每个开发环境生成不同的应用密码。同时,请在<code>wp-config.php</code>文件中将预览令牌定义为常量(例如:<code>define('HEADLESS_PREVIEW_SECRET', '...')</code>),并在mu-plugin中引用这个常量——千万不要将敏感信息硬编码到版本控制下的文件中。</p>
 </div> <section class="mt-12 border-t border-slate-200 pt-10 dark:border-slate-800"><h2 class="text-xl font-bold text-slate-900 dark:text-white">相关文章</h2><div class="mt-6 grid gap-6 md:grid-cols-2"><article class="group flex h-full flex-col overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800 dark:bg-slate-900"> <a href="/articles/how-to-elevate-your-database-game-supercharging-query-performance-with-postgres-fdw" class="relative block aspect-[16/10] overflow-hidden bg-slate-900"> <div class="absolute inset-0 bg-gradient-to-br from-blue-600 via-cyan-700 to-slate-900"> <div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(255,255,255,0.2),transparent_45%)]"></div> </div>   <div class="absolute inset-0 flex flex-col justify-end p-5"> <span class="inline-flex w-fit rounded-full px-3 py-1 text-xs font-semibold bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-200">技术实践</span>  </div> </a> <div class="flex flex-1 flex-col p-5"> <p class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400"> <svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4" data-icon="tabler:calendar">   <use href="#ai:tabler:calendar"></use>  </svg> <time datetime="Sat Feb 21 2026 04:31:05 GMT+0800 (China Standard Time)">2026年2月20日</time>  </p> <h3 class="mt-3 text-lg font-bold leading-snug text-slate-900 dark:text-white"> <a href="/articles/how-to-elevate-your-database-game-supercharging-query-performance-with-postgres-fdw" class="transition hover:text-violet-600 dark:hover:text-violet-300">如何提升数据库的性能:利用Postgres FDW来显著增强查询速度</a> </h3> <p class="mt-3 flex-1 text-sm leading-7 text-slate-600 dark:text-slate-300">外部数据封装机制使得远程的Postgres表看起来就像本地的表一样。正是这种便利 &hellip; Read More</p> <a href="/articles/how-to-elevate-your-database-game-supercharging-query-performance-with-postgres-fdw" class="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-violet-600 transition hover:text-violet-700 dark:text-violet-300">
阅读全文
<svg width="1em" height="1em" class="h-4 w-4 transition group-hover:translate-x-0.5" data-icon="tabler:arrow-right">   <symbol id="ai:tabler:arrow-right" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-6 6l6-6m-6-6l6 6"/></symbol><use href="#ai:tabler:arrow-right"></use>  </svg> </a> </div> </article><article class="group flex h-full flex-col overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800 dark:bg-slate-900"> <a href="/articles/how-to-analyze-insider-transactions-with-python-a-ceo-buying-case-study" class="relative block aspect-[16/10] overflow-hidden bg-slate-900"> <div class="absolute inset-0 bg-gradient-to-br from-blue-600 via-cyan-700 to-slate-900"> <div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(255,255,255,0.2),transparent_45%)]"></div> </div>   <div class="absolute inset-0 flex flex-col justify-end p-5"> <span class="inline-flex w-fit rounded-full px-3 py-1 text-xs font-semibold bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-200">技术实践</span>  </div> </a> <div class="flex flex-1 flex-col p-5"> <p class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400"> <svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4" data-icon="tabler:calendar">   <use href="#ai:tabler:calendar"></use>  </svg> <time datetime="Wed Jul 15 2026 04:24:57 GMT+0800 (China Standard Time)">2026年7月14日</time>  </p> <h3 class="mt-3 text-lg font-bold leading-snug text-slate-900 dark:text-white"> <a href="/articles/how-to-analyze-insider-transactions-with-python-a-ceo-buying-case-study" class="transition hover:text-violet-600 dark:hover:text-violet-300">如何使用Python分析内幕交易:以某位首席执行官的购股行为为例进行案例研究</a> </h3> <p class="mt-3 flex-1 text-sm leading-7 text-slate-600 dark:text-slate-300">当一家公司的股价大幅下跌后,首席执行官却选择购买该公司的股票,人们很容易将这一行 &hellip; Read More</p> <a href="/articles/how-to-analyze-insider-transactions-with-python-a-ceo-buying-case-study" class="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-violet-600 transition hover:text-violet-700 dark:text-violet-300">
阅读全文
<svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4 transition group-hover:translate-x-0.5" data-icon="tabler:arrow-right">   <use href="#ai:tabler:arrow-right"></use>  </svg> </a> </div> </article><article class="group flex h-full flex-col overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800 dark:bg-slate-900"> <a href="/articles/the-react-native-live-activities-handbook-how-to-build-ios-live-activities-and-android-16-live-updates" class="relative block aspect-[16/10] overflow-hidden bg-slate-900"> <div class="absolute inset-0 bg-gradient-to-br from-blue-600 via-cyan-700 to-slate-900"> <div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(255,255,255,0.2),transparent_45%)]"></div> </div>   <div class="absolute inset-0 flex flex-col justify-end p-5"> <span class="inline-flex w-fit rounded-full px-3 py-1 text-xs font-semibold bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-200">技术实践</span>  </div> </a> <div class="flex flex-1 flex-col p-5"> <p class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400"> <svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4" data-icon="tabler:calendar">   <use href="#ai:tabler:calendar"></use>  </svg> <time datetime="Wed Jul 15 2026 04:24:57 GMT+0800 (China Standard Time)">2026年7月14日</time>  </p> <h3 class="mt-3 text-lg font-bold leading-snug text-slate-900 dark:text-white"> <a href="/articles/the-react-native-live-activities-handbook-how-to-build-ios-live-activities-and-android-16-live-updates" class="transition hover:text-violet-600 dark:hover:text-violet-300">《React Native实时活动手册:如何开发iOS实时应用及Android 16版本的实时更新功能》</a> </h3> <p class="mt-3 flex-1 text-sm leading-7 text-slate-600 dark:text-slate-300">“实时活动”功能是指在配送员接近时,该功能会自动显示在锁屏界面上,并且无需你打开 &hellip; Read More</p> <a href="/articles/the-react-native-live-activities-handbook-how-to-build-ios-live-activities-and-android-16-live-updates" class="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-violet-600 transition hover:text-violet-700 dark:text-violet-300">
阅读全文
<svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4 transition group-hover:translate-x-0.5" data-icon="tabler:arrow-right">   <use href="#ai:tabler:arrow-right"></use>  </svg> </a> </div> </article><article class="group flex h-full flex-col overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800 dark:bg-slate-900"> <a href="/articles/how-to-fix-common-web-application-security-vulnerabilities-in-node-js" class="relative block aspect-[16/10] overflow-hidden bg-slate-900"> <div class="absolute inset-0 bg-gradient-to-br from-blue-600 via-cyan-700 to-slate-900"> <div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(255,255,255,0.2),transparent_45%)]"></div> </div>   <div class="absolute inset-0 flex flex-col justify-end p-5"> <span class="inline-flex w-fit rounded-full px-3 py-1 text-xs font-semibold bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-200">技术实践</span>  </div> </a> <div class="flex flex-1 flex-col p-5"> <p class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400"> <svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4" data-icon="tabler:calendar">   <use href="#ai:tabler:calendar"></use>  </svg> <time datetime="Fri Jul 17 2026 04:17:16 GMT+0800 (China Standard Time)">2026年7月16日</time>  </p> <h3 class="mt-3 text-lg font-bold leading-snug text-slate-900 dark:text-white"> <a href="/articles/how-to-fix-common-web-application-security-vulnerabilities-in-node-js" class="transition hover:text-violet-600 dark:hover:text-violet-300">如何修复 Node.js 中常见的 Web 应用程序安全漏洞</a> </h3> <p class="mt-3 flex-1 text-sm leading-7 text-slate-600 dark:text-slate-300">对于那些刚开始接触安全领域的开发者来说,有这样一个现象往往会让他们感到惊讶:大多 &hellip; Read More</p> <a href="/articles/how-to-fix-common-web-application-security-vulnerabilities-in-node-js" class="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-violet-600 transition hover:text-violet-700 dark:text-violet-300">
阅读全文
<svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4 transition group-hover:translate-x-0.5" data-icon="tabler:arrow-right">   <use href="#ai:tabler:arrow-right"></use>  </svg> </a> </div> </article></div></section> </article> <div class="w-full shrink-0 lg:w-56"> <aside class="w-full min-w-0 space-y-8 lg:sticky lg:top-24"> <section> <h2 class="text-sm font-semibold tracking-wide text-slate-900 dark:text-white">内容栏目</h2> <ul class="mt-3 flex flex-col gap-1"> <li> <a href="/articles/category/industry" class="block truncate rounded-lg px-2.5 py-1.5 text-sm font-medium transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300"> 行业洞察 </a> </li><li> <a href="/articles/category/technology" class="block truncate rounded-lg px-2.5 py-1.5 text-sm font-medium transition bg-violet-50 text-violet-700 dark:bg-violet-950/50 dark:text-violet-300"> 技术实践 </a> </li><li> <a href="/articles/category/product" class="block truncate rounded-lg px-2.5 py-1.5 text-sm font-medium transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300"> 产品动态 </a> </li> </ul> </section> <section> <h2 class="text-sm font-semibold tracking-wide text-slate-900 dark:text-white">文章分类</h2> <ul class="mt-3 flex flex-col gap-1"> <li> <a href="/articles/categories/%E5%AE%89%E5%85%A8" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="安全"> 安全 </a> </li><li> <a href="/articles/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="操作系统"> 操作系统 </a> </li><li> <a href="/articles/categories/product" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="产品方案"> 产品方案 </a> </li><li> <a href="/articles/categories/%E4%BA%A7%E5%93%81%E8%BF%90%E8%90%A5" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="产品运营"> 产品运营 </a> </li><li> <a href="/articles/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition bg-violet-50 font-medium text-violet-700 dark:bg-violet-950/50 dark:text-violet-300" title="大数据"> 大数据 </a> </li><li> <a href="/articles/categories/printorder" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="电商打单"> 电商打单 </a> </li><li> <a href="/articles/categories/%E6%95%85%E9%9A%9C%E9%A2%84%E8%AD%A6" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="故障预警"> 故障预警 </a> </li><li> <a href="/articles/categories/%E6%9E%B6%E6%9E%84" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="架构"> 架构 </a> </li><li> <a href="/articles/categories/openapi" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="开放平台"> 开放平台 </a> </li><li> <a href="/articles/categories/uncategorized" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="默认"> 默认 </a> </li><li> <a href="/articles/categories/allsite" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="全渠道OMS+WMS"> 全渠道OMS+WMS </a> </li><li> <a href="/articles/categories/hotnews" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="热点关注"> 热点关注 </a> </li><li> <a href="/articles/categories/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="人工智能"> 人工智能 </a> </li><li> <a href="/articles/categories/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="软件开发"> 软件开发 </a> </li><li> <a href="/articles/categories/%E6%95%B0%E6%8D%AE%E5%BA%93" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="数据库"> 数据库 </a> </li><li> <a href="/articles/categories/%E7%A7%BB%E5%8A%A8" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="移动"> 移动 </a> </li><li> <a href="/articles/categories/%E4%B8%93%E5%AE%B6%E8%A7%82" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="专家观"> 专家观 </a> </li><li> <a href="/articles/categories/expertpoint" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="专家观点"> 专家观点 </a> </li><li> <a href="/articles/categories/iot" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="IoT"> IoT </a> </li> </ul> </section> <section> <h2 class="text-sm font-semibold tracking-wide text-slate-900 dark:text-white">文章标签</h2> <ul class="mt-3 flex flex-wrap gap-1.5"> <li class="max-w-full"> <a href="/articles/tags/%E6%95%B0%E6%8D%AE" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="数据"> 数据 </a> </li><li class="max-w-full"> <a href="/articles/tags/strong" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="strong"> strong </a> </li><li class="max-w-full"> <a href="/articles/tags/data" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="data"> data </a> </li><li class="max-w-full"> <a href="/articles/tags/code" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="code"> code </a> </li><li class="max-w-full"> <a href="/articles/tags/span" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="span"> span </a> </li><li class="max-w-full"> <a href="/articles/tags/ai" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="AI"> AI </a> </li><li class="max-w-full"> <a href="/articles/tags/li" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="li"> li </a> </li><li class="max-w-full"> <a href="/articles/tags/%E6%95%B0%E6%8D%AE%E5%BA%93" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="数据库"> 数据库 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E6%A8%A1%E5%9E%8B" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="模型"> 模型 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BD%BF%E7%94%A8" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="使用"> 使用 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="人工智能"> 人工智能 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%B8%AD" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="中"> 中 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E6%9C%8D%E5%8A%A1" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="服务"> 服务 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BC%9A" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="会"> 会 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E6%8A%80%E6%9C%AF" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="技术"> 技术 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E7%B3%BB%E7%BB%9F" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="系统"> 系统 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E5%AE%89%E5%85%A8" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="安全"> 安全 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E7%94%A8%E6%88%B7" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="用户"> 用户 </a> </li><li class="max-w-full"> <a href="/articles/tags/em" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="em"> em </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BB%A3%E7%A0%81" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="代码"> 代码 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BB%A3%E7%A0%81%EF%BC%8C%E4%BC%9A%E7%9A%84%E3%80%82" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-violet-600 text-white" title="代码,会的。"> 代码,会的。 </a> </li><li class="max-w-full"> <a href="/articles/tags/nbsp" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="nbsp"> nbsp </a> </li><li class="max-w-full"> <a href="/articles/tags/ltr" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="ltr"> ltr </a> </li><li class="max-w-full"> <a href="/articles/tags/variable" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="variable"> variable </a> </li><li class="max-w-full"> <a href="/articles/tags/iot" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="iot"> iot </a> </li><li class="max-w-full"> <a href="/articles/tags/%E8%AE%BE%E5%A4%87" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="设备"> 设备 </a> </li><li class="max-w-full"> <a href="/articles/tags/href" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="href"> href </a> </li><li class="max-w-full"> <a href="/articles/tags/kafka" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="kafka"> kafka </a> </li><li class="max-w-full"> <a href="/articles/tags/python" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="python"> python </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BA%91" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="云"> 云 </a> </li> </ul> </section> </aside> </div> </div> </div>  </main>  <footer class="relative not-prose border-t border-slate-200/80 bg-gradient-to-b from-slate-50 to-white dark:border-slate-800 dark:from-slate-950 dark:to-slate-900"> <div class="relative mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 intersect-once intersect-quarter intersect-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"> <div class="grid grid-cols-12 gap-10 py-12 md:gap-8 md:py-14 lg:gap-12"> <!-- Brand + WeChat --> <div class="col-span-12 lg:col-span-4"> <a class="inline-block" href="/" aria-label="返回首页"> <span class="inline-flex items-end gap-3 self-center whitespace-nowrap"> <img src="/_image?href=%2F_astro%2Fcheeli-logo.DIEHWBc7.png&w=108&h=32&f=webp" loading="eager" decoding="async" alt="Cheeli" style="object-fit:cover;object-position:center;max-width:108px;max-height:32px;aspect-ratio:3.375;width:100%" width="108" height="32" class="block h-8 w-auto shrink-0 invert hue-rotate-180 dark:invert-0 dark:hue-rotate-0"> <span class="relative pb-px pl-3 text-[15px] font-semibold leading-none tracking-[0.14em] text-slate-800 before:absolute before:bottom-0 before:left-0 before:h-3.5 before:w-px before:bg-slate-300/80 dark:text-slate-100 dark:before:bg-slate-600/80 md:text-base">
知力科技
</span> </span> </a> <p class="mt-4 text-sm font-semibold text-slate-800 dark:text-slate-100">蜂巢数字商业平台</p> <p class="mt-2 max-w-sm text-sm leading-6 text-slate-600 dark:text-slate-400">连接商业生态,赋能企业智能经营。</p> <div class="mt-6 inline-flex items-start gap-4 rounded-2xl border border-slate-200/80 bg-white p-3 shadow-sm dark:border-slate-700 dark:bg-slate-900/80"> <img src="/_astro/wechat-cheeli-tech-qrcode.CYWhZFv0.jpg" alt="知力科技官方微信公众号二维码" width="88" height="88" class="block h-[88px] w-[88px] shrink-0 rounded-xl bg-white object-contain" loading="lazy" decoding="async"> <div class="pt-1"> <p class="text-sm font-semibold text-slate-900 dark:text-white">官方公众号</p> <p class="mt-1 text-xs leading-5 text-slate-500 dark:text-slate-400">扫码关注,获取产品动态与行业洞察</p> </div> </div> <div class="mt-6 flex flex-wrap gap-x-4 gap-y-2 text-sm"> <a class="text-slate-500 transition hover:text-blue-600 dark:text-slate-400 dark:hover:text-blue-300" href="/privacy"> 隐私政策 </a><a class="text-slate-500 transition hover:text-blue-600 dark:text-slate-400 dark:hover:text-blue-300" href="/terms"> 服务条款 </a> </div> </div> <!-- Navigation columns --> <div class="col-span-12 grid grid-cols-2 gap-8 sm:grid-cols-3 lg:col-span-8 lg:gap-10"> <div> <p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500"> 产品 </p> <ul class="mt-4 space-y-3"> <li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/products/open-platform"> 蜂巢开放平台 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/products/omnichannel"> 蜂巢全渠道 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/products/ai-agent"> AI智能应用 </a> </li> </ul> </div><div> <p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500"> 解决方案 </p> <ul class="mt-4 space-y-3"> <li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/ecommerce-omnichannel"> 电商全渠道解决方案 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/digital-goods"> 虚拟商品与数字权益解决方案 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/order-fulfillment"> 企业订单履约解决方案 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/private-mall"> 私域商城解决方案 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/digital-connection"> 企业数字化连接解决方案 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/ai-operations"> AI智能运营解决方案 </a> </li> </ul> </div><div> <p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500"> 生态与内容 </p> <ul class="mt-4 space-y-3"> <li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/developers"> 开发者中心 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/cases"> 客户案例 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/articles"> 蜂巢洞察 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/about"> 关于我们 </a> </li> </ul> </div> </div> </div> <!-- Legal bar --> <div class="flex flex-col gap-4 border-t border-slate-200/80 py-6 md:flex-row md:items-center md:justify-between dark:border-slate-800 md:py-7"> <div class="text-xs leading-6 text-slate-500 dark:text-slate-400"> <p class="text-slate-600 dark:text-slate-300">2026 Copyright © 上海知力信息科技有限公司 · All Rights Reserved</p> <p class="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1"> <a class="transition hover:text-blue-600 dark:hover:text-blue-300" href="https://beian.miit.gov.cn/" rel="noopener noreferrer" target="_blank"> 沪ICP备09012674号 </a> <span class="text-slate-300 dark:text-slate-600" aria-hidden="true">|</span> <a class="inline-flex items-center gap-1 transition hover:text-blue-600 dark:hover:text-blue-300" href="https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=31011502018658" rel="noopener noreferrer" target="_blank"> <img src="/_image?href=%2F_astro%2Fbeian-logo.hBd0lRJE.png&w=16&h=16&f=webp" loading="lazy" decoding="async" alt style="object-fit:cover;object-position:center;width:16px;height:16px;object-position:top left" width="16" height="16" class="block h-4 w-4 shrink-0"> 沪公网安备 31011502018658号 </a> <span class="text-slate-300 dark:text-slate-600" aria-hidden="true">|</span> <span>增值电信业务许可证:沪B2-20220251</span> </p> </div> <ul class="flex shrink-0 gap-1 md:justify-end"> <li> <a class="inline-flex items-center rounded-lg p-2.5 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100" aria-label="RSS" href="/rss.xml"> <svg width="1em" height="1em" viewBox="0 0 24 24" class="h-5 w-5" data-icon="tabler:rss">   <use href="#ai:tabler:rss"></use>  </svg>  </a> </li> </ul> </div> </div> </footer>   <aside id="cheeli-float-panel" class="cheeli-float" data-collapsed="false" aria-label="客服联系方式" data-astro-cid-lduetl6a> <button type="button" class="cheeli-float__toggle" data-float-toggle aria-expanded="true" aria-controls="cheeli-float-panel-body" data-astro-cid-lduetl6a> <span class="cheeli-float__toggle-open" aria-hidden="true" data-astro-cid-lduetl6a>收起</span> <span class="cheeli-float__toggle-closed" aria-hidden="true" data-astro-cid-lduetl6a>客服</span> </button> <div id="cheeli-float-panel-body" class="cheeli-float__body" data-astro-cid-lduetl6a> <div class="cf-card cf-card--avatar" data-astro-cid-lduetl6a> <div class="cf-avatar" data-astro-cid-lduetl6a> <img src="/_astro/ocpm.2osPe16p.png" alt="蜂巢客服顾问" width="120" height="130" loading="lazy" decoding="async" data-astro-cid-lduetl6a> </div> <a class="cf-phone" href="tel:400-002-5767" data-astro-cid-lduetl6a>☎ 400-002-5767</a> </div> <div class="cf-card" data-astro-cid-lduetl6a> <img class="cf-qrcode" src="/_astro/wechat.BcEd01Bh.png" alt="客服微信二维码" width="130" height="140" loading="lazy" decoding="async" data-astro-cid-lduetl6a> <div class="cf-title" data-astro-cid-lduetl6a>扫码添加客服微信</div> </div> <a class="cf-btn white" href="https://wpa.qq.com/msgrd?v=3&uin=273785004&site=qq&menu=yes" target="_blank" rel="noopener noreferrer" aria-label="QQ 客服 273785004" data-astro-cid-lduetl6a> <img src="/_astro/qq.Bq_2h58v.png" alt="" width="16" height="18" loading="lazy" decoding="async" data-astro-cid-lduetl6a> <span data-astro-cid-lduetl6a>:273785004</span> </a> </div> </aside>  <script type="module">const c="cheeli-float-contact-collapsed";function d(t){if(t.dataset.bound==="true")return;t.dataset.bound="true";const e=t.querySelector("[data-float-toggle]");if(!e)return;const l=n=>{t.dataset.collapsed=n?"true":"false",e.setAttribute("aria-expanded",n?"false":"true");try{localStorage.setItem(c,n?"1":"0")}catch{}};let a=!1;try{a=localStorage.getItem(c)==="1"}catch{a=!1}l(a),e.addEventListener("click",()=>{l(t.dataset.collapsed!=="true")})}function o(){const t=document.getElementById("cheeli-float-panel");t instanceof HTMLElement&&d(t)}o();document.addEventListener("astro:page-load",o);</script> <script>(function(){const defaultTheme = "system";

  if (window.basic_script) {
    return;
  }

  window.basic_script = true;

  function applyTheme(theme) {
    if (theme === 'dark') {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
  }

  const initTheme = function () {
    if ((defaultTheme && defaultTheme.endsWith(':only')) || (!localStorage.theme && defaultTheme !== 'system')) {
      applyTheme(defaultTheme.replace(':only', ''));
    } else if (
      localStorage.theme === 'dark' ||
      (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
    ) {
      applyTheme('dark');
    } else {
      applyTheme('light');
    }
  };
  initTheme();

  function attachEvent(selector, event, fn) {
    const matches = typeof selector === 'string' ? document.querySelectorAll(selector) : selector;
    if (matches && matches.length) {
      matches.forEach((elem) => {
        elem.addEventListener(event, (e) => fn(e, elem), false);
      });
    }
  }

  const onLoad = function () {
    let lastKnownScrollPosition = window.scrollY;
    let ticking = true;

    attachEvent('#header nav', 'click', function () {
      document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
      document.body.classList.remove('overflow-hidden');
      document.getElementById('header')?.classList.remove('h-screen');
      document.getElementById('header')?.classList.remove('expanded');
      document.getElementById('header')?.classList.remove('bg-page');
      document.querySelector('#header nav')?.classList.add('hidden');
      document.querySelector('#header > div > div:last-child')?.classList.add('hidden');
    });

    attachEvent('[data-aw-toggle-menu]', 'click', function (_, elem) {
      elem.classList.toggle('expanded');
      document.body.classList.toggle('overflow-hidden');
      document.getElementById('header')?.classList.toggle('h-screen');
      document.getElementById('header')?.classList.toggle('expanded');
      document.getElementById('header')?.classList.toggle('bg-page');
      document.querySelector('#header nav')?.classList.toggle('hidden');
      document.querySelector('#header > div > div:last-child')?.classList.toggle('hidden');
    });

    attachEvent('[data-aw-toggle-color-scheme]', 'click', function () {
      if (defaultTheme.endsWith(':only')) {
        return;
      }

      Observer.removeAnimationDelay();

      document.documentElement.classList.toggle('dark');
      localStorage.theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
    });

    attachEvent('[data-aw-social-share]', 'click', function (_, elem) {
      const network = elem.getAttribute('data-aw-social-share');
      const url = encodeURIComponent(elem.getAttribute('data-aw-url'));
      const text = encodeURIComponent(elem.getAttribute('data-aw-text'));

      let href;
      switch (network) {
        case 'facebook':
          href = `https://www.facebook.com/sharer.php?u=${url}`;
          break;
        case 'twitter':
          href = `https://twitter.com/intent/tweet?url=${url}&text=${text}`;
          break;
        case 'linkedin':
          href = `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${text}`;
          break;
        case 'whatsapp':
          href = `https://wa.me/?text=${text}%20${url}`;
          break;
        case 'mail':
          href = `mailto:?subject=%22${text}%22&body=${text}%20${url}`;
          break;

        default:
          return;
      }

      const newlink = document.createElement('a');
      newlink.target = '_blank';
      newlink.href = href;
      newlink.click();
    });

    const screenSize = window.matchMedia('(max-width: 767px)');
    screenSize.addEventListener('change', function () {
      document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
      document.body.classList.remove('overflow-hidden');
      document.getElementById('header')?.classList.remove('h-screen');
      document.getElementById('header')?.classList.remove('expanded');
      document.getElementById('header')?.classList.remove('bg-page');
      document.querySelector('#header nav')?.classList.add('hidden');
      document.querySelector('#header > div > div:last-child')?.classList.add('hidden');
    });

    function applyHeaderStylesOnScroll() {
      const header = document.querySelector('#header[data-aw-sticky-header]');
      if (!header) return;
      if (lastKnownScrollPosition > 60 && !header.classList.contains('scroll')) {
        header.classList.add('scroll');
      } else if (lastKnownScrollPosition <= 60 && header.classList.contains('scroll')) {
        header.classList.remove('scroll');
      }
      ticking = false;
    }
    applyHeaderStylesOnScroll();

    attachEvent([document], 'scroll', function () {
      lastKnownScrollPosition = window.scrollY;

      if (!ticking) {
        window.requestAnimationFrame(() => {
          applyHeaderStylesOnScroll();
        });
        ticking = true;
      }
    });
  };
  const onPageShow = function () {
    document.documentElement.classList.add('motion-safe:scroll-smooth');
    const elem = document.querySelector('[data-aw-toggle-menu]');
    if (elem) {
      elem.classList.remove('expanded');
    }
    document.body.classList.remove('overflow-hidden');
    document.getElementById('header')?.classList.remove('h-screen');
    document.getElementById('header')?.classList.remove('expanded');
    document.querySelector('#header nav')?.classList.add('hidden');
  };

  window.onload = onLoad;
  window.onpageshow = onPageShow;

  document.addEventListener('astro:after-swap', () => {
    initTheme();
    onLoad();
    onPageShow();
  });
})();</script> <script>
  /* Inspired by: https://github.com/heidkaemper/tailwindcss-intersect */
  const Observer = {
    observer: null,
    delayBetweenAnimations: 100,
    animationCounter: 0,
    elements: null,

    start(isNavigation) {
      if (this.observer) {
        this.observer.disconnect();
      }

      this.animationCounter = 0;

      const selectors = [
        '[class*=" intersect:"]',
        '[class*=":intersect:"]',
        '[class^="intersect:"]',
        '[class="intersect"]',
        '[class*=" intersect "]',
        '[class^="intersect "]',
        '[class$=" intersect"]',
      ];

      this.elements = Array.from(document.querySelectorAll(selectors.join(',')));

      const getThreshold = (element) => {
        if (element.classList.contains('intersect-full')) return 0.99;
        if (element.classList.contains('intersect-half')) return 0.5;
        if (element.classList.contains('intersect-quarter')) return 0.25;
        return 0;
      };

      this.elements.forEach((el) => {
        el.setAttribute('no-intersect', '');
        el._intersectionThreshold = getThreshold(el);
      });

      if (isNavigation) {
        const vh = window.innerHeight;
        this.elements.forEach((el) => {
          const rect = el.getBoundingClientRect();
          if (rect.top < vh && rect.bottom > 0) {
            el.removeAttribute('no-intersect');
            el.setAttribute('data-animated', 'true');
            el.style.animationDuration = '0s';
          }
        });
      }

      const callback = (entries) => {
        entries.forEach((entry) => {
          requestAnimationFrame(() => {
            const target = entry.target;
            const intersectionRatio = entry.intersectionRatio;
            const threshold = target._intersectionThreshold;

            if (target.classList.contains('intersect-no-queue')) {
              if (entry.isIntersecting) {
                target.removeAttribute('no-intersect');
                if (target.classList.contains('intersect-once')) {
                  this.observer.unobserve(target);
                }
              } else {
                target.setAttribute('no-intersect', '');
              }
              return;
            }

            if (intersectionRatio >= threshold) {
              if (!target.hasAttribute('data-animated')) {
                target.removeAttribute('no-intersect');
                target.setAttribute('data-animated', 'true');
                target.style.animationDuration = '';

                const delay = this.animationCounter * this.delayBetweenAnimations;
                this.animationCounter++;

                target.style.transitionDelay = `${delay}ms`;
                target.style.animationDelay = `${delay}ms`;

                if (target.classList.contains('intersect-once')) {
                  this.observer.unobserve(target);
                }
              }
            } else {
              target.setAttribute('no-intersect', '');
              target.removeAttribute('data-animated');
              target.style.transitionDelay = '';
              target.style.animationDelay = '';
              target.style.animationDuration = '';

              this.animationCounter = 0;
            }
          });
        });
      };

      this.observer = new IntersectionObserver(callback, { threshold: [0, 0.25, 0.5, 0.99] });

      this.elements.forEach((el) => {
        if (!el.hasAttribute('data-animated')) {
          this.observer.observe(el);
        }
      });
    },

    /*
      REF: #643;
      We need to remove the delay to fix flickering/delay
      when toggling the theme. Observer only removes them
      after data-animated is gone (out of view).
    */
    removeAnimationDelay() {
      this.elements.forEach((el) => {
        if (el.getAttribute('data-animated') === 'true') {
          el.style.transitionDelay = '';
          el.style.animationDelay = '';
        }
      });
    },
  };

  Observer.start(false);

  document.addEventListener('astro:after-swap', () => {
    Observer.start(true);
  });
</script> </body></html>