← 返回蜂巢洞察

如何利用浏览器使用数据以及Claude API来构建一个本地SEO审计工具

每个数字营销机构都有人负责这样的工作:打开电子表格,访问每一个客户的网站地址,检 … Read More

Socrates

每个数字营销机构都有人负责这样的工作:打开电子表格,访问每一个客户的网站地址,检查标题标签、元描述以及H1标签,记录下所有失效的链接,然后将这些信息整理成报告。下周再重复这个过程。

这种工作具有确定性,任何员工都能完成它。

在本教程中,你将使用Python、Browser Use以及Claude API从零开始构建一个用于进行本地SEO审计的工具。该工具会在可视化的浏览器窗口中访问真实的网页,利用Claude提取相关的SEO数据,异步检查失效链接,在遇到特殊情况时会暂停执行并等待人工干预,最后生成结构清晰的报告——即使中途中断,也可以继续后续操作。

完成制作后,你将拥有一个可以用于处理任意URL列表的工具。每个URL的运行成本不到0.01美元。

你将构建什么

这是一个由七个模块组成的Python工具,它可以:

  • 从CSV文件中读取URL列表

  • 在真实的Chromium浏览器中访问每一个URL(而不是使用无头爬虫程序)

  • 通过Claude API提取标题、元描述、H1标签以及规范链接标签

  • 使用httpx异步检查失效链接

  • 检测特殊情况(如404错误、需要登录才能访问的页面、重定向等),并在必要时暂停执行以等待人工输入

  • 将处理结果逐步写入report.json文件中——即使中途中断,也可以继续后续操作

  • 在任务完成后生成一份用通俗语言编写的report-summary.txt报告

完整的代码可以在GitHub上找到,地址为:dannwaneri/seo-agent

先决条件

  • Python 3.11或更高版本

  • Anthropic API密钥(可在console.anthropic.com获取)

  • Windows、macOS或Linux操作系统

  • 对Python及命令行有一定的了解

目录

  1. 为什么使用浏览器而不是爬虫程序

  2. 项目结构

  3. 准备工作

  4. 模块1:状态管理

  5. 模块2:浏览器集成

  6. 模块3:Claude数据提取层

  7. 模块4:失效链接检查器

  8. 模块5:人工干预机制

  9. 模块6:报告生成器

  10. 模块7:主循环流程

  11. 运行该工具

  12. 为机构使用而进行调度安排

  13. 结果展示方式

为什么使用浏览器而不是爬虫

进行SEO审计的标准方法是使用requests获取页面的HTML内容,然后利用BeautifulSoup对其进行解析。这种方法适用于静态页面,但对于由JavaScript生成的页面来说则无法正常工作;它还会遗漏动态插入的元标签,而在需要身份验证的页面上更是完全无法使用。

“Browser Use”采用了不同的方法:它控制一个真实的Chromium浏览器,在JavaScript执行完毕后再读取DOM结构,并通过Playwright的工具树来呈现页面内容。这样,该工具就能像人类一样看到页面的实际显示效果。

实际应用中的区别在于:基于requests

另一个值得注意的区别是:“Browser Use”能够以语义化的方式解析页面内容。例如,当某个按钮的CSS类从btn-primarybutton-main

项目结构

seo-agent/
├── index.py          # 主要审计流程
├── browser.py        # 使用浏览器进行页面解析的模块
├── extractor.py      # 用于与Claude API交互的数据提取层
├── linkchecker.py    # 异步检测失效链接的工具
├── hitl.py           | 实时暂停机制
├── reporter.py       | 报告生成工具
├── state.py          | 状态保存机制(可在中断后继续执行)
├── input.csv         | 需要审计的URL列表
├── requirements.txt
├── .env.example
└── .gitignore

安装与配置

首先创建一个项目文件夹,然后安装所需的依赖项:

mkdir seo-agent && cd seo-agent
pip install browser-use anthropic playwright httpx
playwright install chromium

接下来创建input.csv文件,将需要审计的URL列表保存其中:

url
https://example.com
https://example.com/about
https://example.com/contact

然后创建.env.example文件,并设置ANTHROPIC_API_KEY环境变量:

ANTHROPIC_API_KEY=your-key-here

在运行程序之前,请确保已将API密钥设置为相应的环境变量:

# macOS/Linux
export ANTHROPIC_API KEY="sk-ant-..."
# Windows PowerShell
$env:ANTHROPIC_API_KEY = "sk-ant-..."

最后创建.gitignore文件,指定哪些文件不需要被版本控制工具跟踪:

state.json
report.json
report-summary.txt
.env
__pycache__/
*.pyc

模块1:状态管理

该工具需要记录已经审计过的URL列表。如果审计过程被中断——无论是由于停电、键盘按键被按下还是网络故障——它都应该从上次停止的地方继续执行,而不是重新开始。

state.py文件通过一个简单的JSON文件来实现这一功能:

import json
import os

STATE_FILE = os.path.join(os.path.dirname(__file__), "state.json")

_DEFAULT_STATE = {"audited": [], "pending": [], "needs_human": []}

def load_state() -> dict:
    if not os.path.exists(STATE_FILE):
        save_state(_DEFAULT_STATE.copy())
    with open(STATE_FILE, encoding="utf-8") as f:
        return json.load(f)

def save_state(state: dict) -> None:
    with open(STATE_FILE, "w", encoding="utf-8") as f:
        json.dump(state, f, indent=2)

def is_audited(url: str) -> bool:
    return url in load_state()["audited"]

def mark_audited(url: str) -> None:
    state = load_state()
    if url not in state["audited"]:
        state["audited"].append(url)
    save_state(state)

def add_to_needs_human(url: str) -> None:
    state = load_state()
    if url not in state["needs_human":
        state["needs_human"].append(url)
    save_state(state)

这一设计是经过深思熟虑的:mark_audited()函数会在处理完某个URL并将其写入报告之后立即被调用。如果代理程序在运行过程中崩溃,那么它最多只会丢失对那个URL的处理结果。

模块2:浏览器集成

browser.py负责实际的页面导航操作。它直接使用Playwright来打开一个可视化的Chromium窗口,导航到指定的URL,捕获HTTP状态码及重定向信息,并从DOM中提取原始的SEO数据。

一些关键的设计决策包括:

使用可视化浏览器,而非无头浏览器。headless=False设置为True,这样就可以观察到代理程序的实际运行过程。这对于演示和调试来说非常重要。

通过响应监听器来捕获状态信息。当收到4xx或5xx类型的响应时,Playwright会抛出异常,但on("response", ...)处理函数会在异常发生之前被执行,因此我们可以在那里获取到状态码信息。

每次访问之间会间隔2秒钟。这样就可以避免触发代理程序所在客户端的速率限制机制或机器人检测系统。

以下是核心的页面导航功能代码:

import asyncio
import sys
import time
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout

TIMEOUT = 20_000  # 20秒


def fetch_page(url: str) -> dict:
    result = {
        "final_url": url,
        "status_code": None,
        "title": None,
        "meta_description": None,
        "h1s": [],
        "canonical": None,
        "raw_links": [],
    }

    first_status = {"code": None}

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        page = browser.new_page()

        def on_response(response):
            if first_status["code"] is None:
                first_status["code"] = response.status

        page.on("response", on_response)

        try:
            page.goto(url, wait_until="domcontentloaded", timeout=TIMEOUT)
            result["status_code"] = first_status["code"] or 200
            result["final_url"] = page.url

            # 从DOM中提取SEO数据
            result["title"] = page.title() or None
            result["meta_description"] = page.evaluate(
                "() => { const m = document.querySelector('meta[name=\"description\"]'); "
                "return m ? m.getAttribute('content') : null; }"
            )
            result["h1s"] = page.evaluate(
                "() => Array.from(document.querySelectorAll('h1')).map(h => h.innerText.trim())"
            )
            result["canonical"] = page.evaluate(
                "() => { const c = document.querySelector('link[rel=\"canonical\"]'); "
                "return c ? c.getAttribute('href') : null; }"
            )
            result["raw_links"] = page.evaluate(
                "() => Array.from(document.querySelectorAll('a[href]'))"
                ".map(a => a.href).filter(Boolean).slice(0, 100)"
            )

        except PlaywrightTimeout:
            result["status_code"] = first_status["code"] or 408
        except Exception as exc:
            print(f"[browser] 错误:{exc}", file=sys.stderr)
            result["status_code"] = first_status["code"]
        finally:
            browser.close()

    time.sleep(2)
    return result

有几点需要注意:

raw_links的限制为100个链接,这是有意为之。DEV.to的个人主页页面上包含数百个链接——在进行失效链接检测时,并不需要使用所有这些链接。

wait_until="domcontentloaded"这个设置比networkidle更快,而且对于提取元标签来说已经足够了。由JavaScript渲染的内容需要等到DOM结构准备好才能被处理,而不是等到所有的网络请求都完成。

模块3:Claude提取层

extractor.py会从browser.py中获取原始页面数据,然后调用Claude来生成结构化的SEO审计结果。

大多数教程在这个环节都会出错。它们要么在Python中编写复杂的解析逻辑(这种做法很不可靠),要么让Claude返回非结构化的文本,然后再尝试解析这些文本(同样不可靠)。正确的做法是:为Claude提供一个严格的JSON格式规范,并要求它只返回符合这个规范的结果。

正是这种精确的提示设计才使得这一过程能够可靠地运行:

import json
import os
import sys
from datetime import datetime, timezone
import anthropic

MODEL = "claude-sonnet-4-20250514"
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))


def _strip_fences(text: str) -> str:
    """从Claude的回复中去除多余的markdown格式标签。"""
    text = text.strip()
    if text.startswith("```"):
        lines = text.splitlines()
        # 删除开头的标记
        lines = lines[1:] if lines[0].startswith("```") else lines
        # 删除结尾的标记
        if lines and lines[-1].strip() == "```":
            lines = lines[:-1]
        text = "\n".join(lines).strip()
    return text


def extract(snapshot: dict) -> dict:
    if not os.environ.get("ANTHROPIC_API_KEY"):
        raise OSError("ANTHROPIC_API_KEY未设置。")

    prompt = f"""你是一名SEO审计员。请分析这个页面数据,并仅返回一个JSON对象。
不要包含任何散文性内容,也不要添加解释或markdown格式标签。只返回原始的JSON数据。

页面信息:
- URL: {snapshot.get('final_url')}
- 状态码: {snapshot.get('status_code')]
- 标题: {snapshot.get('title'}
- 元描述: {snapshot.get('meta_description'}
- H1标题: {snapshot.get('h1s'}
- 规范链接: {snapshot.get('canonical')}

返回以下格式的JSON对象:
{{
  "url": "string",
  "final_url": "string",
  "status_code": number,
  "title": {{"value": "string or null", "length": number, "status": "PASS or FAIL"},
  "description": {{"value": "string or null", "length": number, "status": "PASS or FAIL"},
  "h1": {{"count": number, "value": "string or null", "status": "PASS or FAIL"},
  "canonical": {{"value": "string or null", "status": "PASS or FAIL"},
  "flags": ["array of strings describing specific issues"],
  "human_review": false,
  "audited_at": "ISO timestamp"
}}

PASS/FAIL规则:
- 标题:如果为空或长度超过60个字符,则视为失败
- 描述:如果为空或长度超过160个字符,则视为失败
- H1标题:如果数量为0(缺失)或数量大于1(重复出现),则视为失败
- 规范链接:如果为空,则视为失败
- flags:列出所有出错的字段,并附上详细的说明
- audited_at:使用当前UTC时间的ISO 8601格式"

    response = client.messages.create(
        model=MODEL,
        max_tokens=1000,
        messages=[{"role": "user", "content": prompt}],
    )

    raw = response.content[0].text
    clean = _strip_fences(raw)

    try:
        return json.loads(clean)
    except json.JSONDecodeError as exc:
        print(f"[extractor] JSON解析错误:{exc}", file=sys.stderr)
        return _error_result(snapshot, str(exc))


def _error_result.snapshot: dict, reason: str) -> dict:
    return {
        "url": snapshot.get("final_url", ""),
        "final_url": snapshot.get("final_url", ""),
        "status_code": snapshot.get("status_code"),
        "title": {"value": None, "length": 0, "status": "ERROR"},
        "description": {"value": None, "length": 0, "status": "ERROR"},
        "h1": {"count": 0, "value": None, "status": "ERROR"},
        "canonical": {"value": None, "status": "ERROR"},
        "flags": [f"提取错误:{reason}"],
        "human_review": True,
        "audited_at": datetime.now(timezone.utc).isoformat(),
    }

有两点确保了这一功能在实际生产环境中的可靠性:

首先,`_strip_fences()`函数能够处理这样一种情况:尽管被明确要求不要这样做,但Claude仍然会将其响应用`json`格式的标签括起来。这种情况在Sonnet中偶尔会发生,如果不加以处理,就会导致`json.loads()`函数出现错误。

其次,`_error_result()`函数能够确保代理程序在接收到Claude返回的错误响应时不会崩溃——它会记录错误信息,并将相关URL标记出来供人工审核,然后继续处理下一个URL。

成本:Claude Sonnet 4的服务费用为:每百万输入字符3美元,每百万输出字符15美元。一般来说,一个页面的快照大约需要500个输入字符;而结构化的JSON响应则大约需要300个输出字符。因此,对于一个包含20个URL的审计任务来说,总成本大概为0.006美元/URL,也就是0.12美元。

linkchecker.py会从浏览器快照中获取`raw_links`列表,然后使用异步HEAD请求来检查这些链接是否有效。

在设计上我们做了以下几项选择:

  • 仅检测同一域名的链接。

    如果检查页面上的所有外部链接,将会花费很长时间,而这也不是代理客户所需要的。因此,我们只检查与被审计页面属于同一域名的链接。

  • 使用HEAD请求而非GET请求。

    HEAD请求速度更快,占用的带宽也更少,完全足以用来检测链接的状态码。

  • 每次最多检测50个链接。

    像DEV.to这样的网站,其文章列表中通常会包含数百个内部链接。如果全部检查这些链接,将会严重影响程序的运行效率。

  • 通过asyncio同时处理多个请求。

    所有链接都会被并行检测,而不是依次处理。

import asyncio
import logging
from urllib.parse import urlparse
import httpx

CAP = 50
TIMEOUT = 5.0
logger = logging.getLogger(__name__)

def _same_domain(link: str, final_url: str) -> bool:
if not link:
return False
lower = link.strip().lower()
if lower.startswith("#

or "mailto:"
or "javascript:"
or "tel:"
or "data:"):
return False
try:
page_host = urlparse(final_url).netloc.lower()
parsed = urlparse(link)
return parsedscheme in ("http", "https") and parsed.netloc.lower() == page_host
except Exception:
return False

async def _check_link(client: httpx.AsyncClient, url: str) -> tuple[str, bool]:
try:
resp = await client.head(url, follow_redirects=True, timeout=TIMEOUT)
return url, resp.status_code != 200
except Exception:
return url, True # 表示请求超时或连接失败,因此链接无效

async def _run_checks(links: list[str]) -> list[str]:
async with httpx.AsyncClient() as client:
results = await asyncio.gather(*[_check_link(client, url) for url in links])
return [url for url, broken in results if broken]

def check_links(raw_links: list[str], final_url: str) -> dict:
same_domain = [l for l in raw_links if _same_domain(l, final_url)]

capped = len(same_domain) > CAP
if capped:
logger.warning("页面中包含%d个同一域名的链接,但最多只能检测%d个。",
len(same_domain), CAP)
same_domain = same_domain[:CAP]

broken = asyncio.run(_run_checks(same_domain))

return {
"broken": broken,
"count": len(broken),
"status": "FAIL" if broken else "PASS",
"capped": capped,
}

模块5:人工干预机制

大多数自动化教程都会跳过这一部分。当程序遇到登录障碍时会发生什么?当页面返回403错误代码时怎么办?或者当链接跳转到“订阅以继续阅读”页面时又该如何处理?

大多数脚本在这种情况下要么会崩溃,要么会默默地忽略这些异常。但在实际应用中,这两种情况都是不可接受的。

hitl.py通过两个函数来解决这些问题:一个函数用于判断是否需要暂停操作,另一个函数则负责执行暂停操作本身。

from state import add_to_needs_human

LOGIN_KEYWORDS = {"login", "sign in", "sign-in", "access denied", "log in", "unauthorized"}
REDIRECT_CODES = {301, 302, 307, 308}

def should_pause(snapshot: dict) -> bool:
    code = snapshot.get("status_code")

    # 如果导航完全失败
    if code is None:
        return True

    # 如果状态码不是200,且不属于重定向代码
    if code != 200 and code not in REDIRECT_CODES:
        return True

    # 检查标题或H1标签中是否包含登录相关关键词
    title = (snapshot.get("title") or "").lower()
    h1s = [h.lower() for h in (snapshot.get("h1s") or [])]

    if any(kw in title for kw in LOGIN_KEYWORDS):
        return True
    if any(kw in h1 for kw in LOGIN_keywords for h1 in h1s):
        return True

    return False


def pause_reason(snapshot: dict) -> str:
    code = snapshot.get("status_code")
    if code is None:
        return "导航失败(状态码未知)"
    if code != 200 and code not in REDIRECT_CODES:
        return f"出现异常状态码:{code}"
    return "可能遇到了登录障碍"

should_pause()函数可以检测四种情况:导航失败、出现非预期的HTTP状态码、标题中包含登录相关关键词,以及H1标签中包含这些关键词。其中,对标题的检查能够有效识别那些虽然返回200状态码但实际上无法访问的页面。

--auto模式下(用于定时执行任务),主循环会直接跳过pause_and_prompt()函数的调用,而是将相关信息记录到needs_human[]数组中,然后继续执行后续操作。

模块6:报告生成工具

reporter.py会逐个记录处理结果。这一点非常重要:因为结果是在每次审核完一个URL后立即被记录下来的,而不是在所有操作完成后才一次性生成。这样一来,如果任务中途被中断,之前完成的工作也不会丢失。

import json import os from datetime import datetime, timezone REPORT_JSON = os.path.join(os.path.dirname(__file__), "report.json") REPORTTXT = os.path.join(os.path.dirname(__file__), "report-summary.txt") def _load_report() -> list: if not os.path.exists(REPORT_json): return [] with open(REPORT_JSON, encoding="utf-8") as f: return json.load(f) def write_result(result: dict) -> None: """在 report.json 文件中添加或更新相关数据。""" entries = _load_report() url = result.get("url", "") # 如果该 URL 已经存在于文件中,则更新相应的记录(支持重试机制) for i, entry in enumerate(entries): if entry.get("url") == url: entries[i] = result break else: entries.append(result) with open(REPORT_JSON, "w", encoding="utf-8") as f: json.dumpentries, f, indent=2, ensure_ascii=False) def _is_overall_pass(result: dict) -> bool: fields = ["title", "description", "h1", "canonical"] for field in fields: if result.get(field, {}).get("status") not in ("PASS",): return False if result.get("broken_links", "").get("status") == "FAIL": return False return True def write_summary() -> None: entries = _load_report() passed = sum(1 for e in entries if _is_overall_pass(e)) lines = [] for entry in entries: overall = "PASS" if _is_overall_pass(entry) else "FAIL" failed_fields = [ f for f in ["title", "description", "h1", "canonical", "broken_links"] if entry.get(f, "").get("status") == "FAIL" ] suffix = f" [{', '.join(failed_fields)}]" if failed_fields else "" lines.append(f"{entry.get('url', 'unknown'):<60} | {overall}{suffix}") lines.append("") lines.append(f"{passed}/{len(entries)} 个 URL 通过了检测") with open(REPORTTXT, "w", encoding="utf-8") as f: f.write("\n".join(lines))

write_result()中的去重机制能够妥善处理重试操作。如果在人工审核并完成身份验证后再次尝试访问某个URL,系统会用新的审核结果替换原有的记录,而不会创建重复的条目。

模块7:主循环

index.py负责将所有组件连接在一起。它读取URL列表,加载状态信息,跳过已经完成审核的URL,然后启动审核流程。

import csv
import os
import sys
import time
import argparse

from state import load_state, is_audited, mark_audited, add_to_needs_human
from browser import fetch_page
from extractor import extract
from linkchecker import check_links
from hitl import should_pause, pause_reason, pause_and_prompt
from reporter import write_result, write_summary

INPUT_csv = os.path.join(os.path.dirname(__file__), "input.csv")

def read_urls(path: str) -> list[str]:
    with open(path, newline="", encoding="utf-8") as f:
        return 
.strip() for row in csv.DictReader(f) if row.get("url", "").strip()]
def run(auto: bool = False): if not os.environ.get("ANTHROPIC_API_KEY"): print("错误:未设置ANTHROPIC_API KEY环境变量。") sys.exit(1) urls = read_urls(INPUT_csv) pending = [u for u in urls if not is_audited(u)] print(f"开始审核:还有{len(pending)}个URL待审核,已有{len(urls) - len(pending)}个URL完成审核。\n") total = lenurls) try: for i, url in enumerate(pending, start=1): position = urls.index(url) + 1 print(f"[{position}/{total}] {url}", end=" -> ", flush=True) # 打开浏览器访问该URL snapshot = fetch_page(url) # 进行人工审核 if should_pause(snapshot): reason = pause_reason(snapshot) if auto: print(f"自动跳过({reason})") add_to_needs_human(url) mark_audited(url) continue action = pause_and_prompt(url, reason) if action == "quit": print("退出审核流程。") break elif action == "skip": add_to_needs_human(url) mark_audited(url) continue # 如果选择“retry”,则重新尝试访问该URL snapshot = fetch_page(url) # 使用Claude工具提取信息 result = extract(snapshot) # 检查是否存在失效的链接 links = check_links(snapshot.get("raw_links", []), snapshot.get("final_url", url)) result["broken_links"] = links # 立即写入审核结果 write_result(result) mark_audited(url) overall = "PASS" if all( result.get(f, "").get("status") == "PASS" for f in ["title", "description", "h1", "canonical"] ) and links["status"] == "PASS" else "FAIL" print(overall) except KeyboardInterrupt: print("\n审核过程被中断。已保存进度,重新运行即可继续。") return write_summary() passed = sum( 1 for e in [r for r in []] if all(e.get(f, "").get("status") == "PASS" for f in ["title", "description", "h1", "canonical"]) ) print(f"\n审核完成。报告已保存至report.json和report-summary.txt文件中。") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--auto", action="store_true", help="自动跳过需要人工审核的URL") args = parser.parse_args() run(auto=args.auto)

KeyboardInterrupt处理程序就是用于恢复程序运行的机制。当你按下Ctrl+C时,该处理程序会输出一条消息并干净地退出程序。由于对于每个URL来说,在write_result()方法执行之后都会调用mark_audited()方法,因此下次运行程序时会跳过已经处理过的所有内容。

运行代理程序

交互模式(在遇到特殊情况时会暂停程序运行):

python index.py

自动模式(会跳过特殊情况,并将未处理的URL添加到needs_human[]列表中):

python index.py --auto

当程序运行时,你会看到每个URL对应的浏览器窗口会被打开,同时终端也会显示进度信息:

开始审计:还有7个URL未处理,0个已经完成。
[1/7] https://example.com -> 通过
[2/7] https://example.com/about -> 失败
[3/7] https://example.com/contact -> 被自动跳过(返回的状态码为404)
…
审计已完成。报告文件已保存为report.json和report-summary.txt

如果程序运行过程中被中断,可以重新运行它:

python index.py --auto
# 开始审计:还有4个URL未处理,3个已经完成。

为机构使用安排调度任务

对于需要每周定期执行的审计任务,可以创建一个批处理文件,然后通过Windows任务计划程序来安排执行。

创建run-audit.bat文件:

@echo off
set ANTHROPIC_API_KEY=你的API密钥
cd /d C:\Users\你的用户名\Desktop\seo-agent
python index.py --auto

在Windows任务计划程序中,按照以下步骤操作:

  1. 创建一个新的基本任务。

  2. 将触发条件设置为“每周一上午7点”。

  3. 将动作设置为“启动一个程序”。

  4. 选择run-audit.bat文件作为要执行的程序。

周一早上查看report-summary.txt文件。其中那些被标记为needs_human[]的URL需要人工审核——比如那些需要输入用户名或密码才能访问的页面,或者返回了异常状态码的页面。

对于macOS/Linux系统,可以使用cron来安排任务:

# 每周一上午7点运行
0 7 * * 1 cd /path/to/seo-agent && ANTHROPIC_API_KEY=你的API密钥 python index.py --auto

审计结果是什么样的

我使用这个代理程序对我的在Hashnode、freeCodeCamp和DEV.to上发布的7个页面进行了检测,结果所有这些页面都失败了。

https://hashnode.com/@dannwaneri                    | 失败 [标题]
https://freecodecamp.org/news/claude-code-skill     | 失败 [描述]
https://freecodecamp.org/news/stop-letting-ai-guess | 失败 [描述]
https://freecodecamp.org/news/rag-system-handbook   | 失败 [标题, 描述]
https://freecodecamp.org/news/author/dannwaneri     | 失败 [描述]
https://dev.to/dannwaneri/gatekeeping-panic         | 失败 [标题]
https://dev.to/dannwaneri/production-rag-system     | 失败 [标题]

0/7个URL通过审核

freeCodeCamp在描述文章时存在的问题,部分是由于平台本身的限制所致——freeCodeCamp使用的模板有时会截断或省略文章列表页面的元描述信息。而DEV.to在标题处理方面存在的问题则出在我自己身上,那些被用作标题的文章,其</code>标签中的字符长度往往超过了60个。</p> <p>需要说明的是,这个60个字符的长度限制其实只是一个显示上的限制,并不会影响文章的排名。谷歌会收录所有长度的标题。设定这一限制主要是为了确保在桌面端搜索结果中,标题能够完整显示而不会被截断。如果标题超过60个字符,虽然仍然会被展示出来,但可能会被截断,从而影响用户的点击率。系统标记这些标题只是为了提醒开发者注意显示上的问题,并不意味着这些标题违反了排名规则。</p> <h2 id="heading-next-steps">后续步骤</h2> <p>目前这个工具已经能够完成基本的SEO审计工作流程。不过还可以进行一些扩展功能,例如:</p> <ul> <li> <p><strong>性能指标检测</strong> — 可以为每个URL添加Lighthouse或PageSpeed Insights的API调用。</p> </li> <li> <p><strong>结构化数据验证</strong> — 检查页面中是否使用了JSON-LD格式进行数据标记,并对其进行验证。</p> </li> <li> <p><strong>邮件发送功能</strong> — 审计完成后,可以通过SMTP发送<code>report-summary.txt</code>文件。</p> </li> <li> <p><strong>多客户支持</strong> — 可以为不同的客户分别准备<code>input.csv</code>文件,并生成不同的报告目录。</p> </li> <p>包含所有七个模块的完整代码可以在<a href="https://github.com/dannwaneri/seo-agent">dannwaneri/seo-agent</a>这个链接找到。你可以克隆这个代码,添加你自己的URL地址,然后运行它。</p> <p><em>如果你觉得这篇文章有用,我还会在<em>DEV.to/@dannwaneri</em><em>这个平台上撰写更多关于如何为开发人员和机构配置AI工具的文章。</em><em>DEV.to上还有关于这个工具设计理念的详细介绍,包括为什么使用HITL算法、为什么优先选择浏览器数据而不是爬虫数据,以及审计结果对你自己发布的文章意味着什么。</em></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/relaxation-and-its-role-in-vision-the-1977-phd-thesis-that-helped-shape-modern-ai-research" 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 22 2026 14:41:11 GMT+0800 (China Standard Time)">2026年7月22日</time> </p> <h3 class="mt-3 text-lg font-bold leading-snug text-slate-900 dark:text-white"> <a href="/articles/relaxation-and-its-role-in-vision-the-1977-phd-thesis-that-helped-shape-modern-ai-research" class="transition hover:text-violet-600 dark:hover:text-violet-300">“放松与它在视觉中的作用”:这篇1977年完成的博士论文为现代人工智能研究的发展奠定了基础</a> </h3> <p class="mt-3 flex-1 text-sm leading-7 text-slate-600 dark:text-slate-300">当人们提到杰弗里·辛顿时,他们通常会想到反向传播算法、玻尔兹曼机、深度信念网络,以及那些彻底改变了人工智能发展的深度学习技术。 然而,很少有人会追溯到他科研生涯的起点。 1977年,在那篇著名的关于反向传播的论文发表之前的近十年时间里,辛顿在爱丁堡大学完成了他的博士论文,题为《松弛机制在视觉认知中的作用》。乍一看,这篇论文似乎只是探讨计算机视觉与松弛算法之间的关系。当我开始阅读它时,我也正是抱着这样的预期。 然而,在仔细研读之后,我意识到这篇论文涉及的远不止视觉算法。许多后来成为辛顿研究核心的理念,在当时就已经初具雏形。虽然当时的术语有所不同,数学表达也更为简单,神经网络也尚未成为他研究的重点</p> <a href="/articles/relaxation-and-its-role-in-vision-the-1977-phd-thesis-that-helped-shape-modern-ai-research" 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/the-hidden-tax-of-infrastructure-why-your-team-shouldnt-be-running-it-anymore" 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 Apr 25 2026 18:06:46 GMT+0800 (China Standard Time)">2026年4月25日</time> </p> <h3 class="mt-3 text-lg font-bold leading-snug text-slate-900 dark:text-white"> <a href="/articles/the-hidden-tax-of-infrastructure-why-your-team-shouldnt-be-running-it-anymore" class="transition hover:text-violet-600 dark:hover:text-violet-300">那些被隐藏起来的基础设施建设成本:为什么你的团队不应该再继续承担这些责任了</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-hidden-tax-of-infrastructure-why-your-team-shouldnt-be-running-it-anymore" 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-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" 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-impact-of-ai-on-software-testing" 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="Tue Mar 05 2024 10:18:08 GMT+0800 (China Standard Time)">2024年3月5日</time> </p> <h3 class="mt-3 text-lg font-bold leading-snug text-slate-900 dark:text-white"> <a href="/articles/the-impact-of-ai-on-software-testing" class="transition hover:text-violet-600 dark:hover:text-violet-300">人工智能对软件测试的影响</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-impact-of-ai-on-software-testing" 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-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/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>