n8n创意写作AI 评分 · 82/100AI 辅助编辑·未发现明显提示词注入风险n8n.io

批量 Google PageSpeed Insights 转 CSV

在 n8n 中通过聊天输入多个 URL,自动调用 PageSpeed API 获取性能指标,输出可下载的 CSV 报告

有一点经验步骤流程提示词
给谁
需要批量检测网页 Core Web Vitals 的 SEO 人员、前端开发者或运维
你会得到
一次最多处理 10 个 URL,生成包含 LCP、FCP、CLS、Speed Index 等指标的 CSV 文件,通过临时链接下载
你需要
n8n · google-pagespeed-insights

硬约束

前置条件:n8n 实例(自托管或云版)、Google PageSpeed Insights API Key(通过 httpQueryAuth 凭证配置)。输入为聊天消息中的 URL 列表(空格或逗号分隔)。输出为 uguu.se 托管的临时 CSV 下载链接。限制:每批最多 10 个 URL;API 超时 90 秒;临时文件 24-48 小时后失效。

他们做了什么

作者 siddharth 在 n8n 模板库发布了一个工作流模板,通过聊天界面接收 URL 列表,自动完成验证、可达性检查、PageSpeed API 批量调用、数据解析和 CSV 导出。模板包含 44 个节点(含 25 个注释便签),分为输入验证、预检、API 循环、输出交付四个阶段。

效果如何

模板自述效果(author_report):用户粘贴 URL 列表后,工作流自动完成全部分析并返回可下载的 CSV 链接。未提供具体处理时长或成功率数字。

值得知道

每批最多 10 个 URL;API 请求超时 90 秒;输出文件托管在 uguu.se,通常 24-48 小时后失效。必须配置 Google API Key,否则易触发 429 速率限制。处理 50+ URL 可能需要数分钟,执行期间不要关闭聊天窗口。本站未实测该工作流。

n8n 工作流模板

结构已检查 · 未实际运行

下载后在 n8n 选择“Import from File”,或复制 JSON 后粘贴到编辑器。先配置自己的账号,再用测试数据运行;模板默认未启用。

配置与测试清单

需要绑定的凭证

  • PageSpeed API 节点:绑定httpQueryAuth 凭证。

需要替换的配置

  • 源模板来自 n8n 公开库 #15548(约 44 个节点)。导入后先保持未启用,核对触发器、表达式与外部 API 端点。
  • Sticky Note 中的说明仅供参考;按自己的账号、表结构或频道 ID 替换占位配置。

测试输入与预期结果

  • 在测试环境手动执行一次主路径,确认每个需凭证的节点已连接且无鉴权错误。
  • 用一条合成/脱敏样例数据跑通,核对输出字段与下游写入目标,再考虑生产启用。

模板处理说明

  • 已移除原作者凭证绑定、webhookId 与执行数据,工作流设为未启用;节点、连线与原文提示词保持原样。未在 n8n 中实际运行。
  • 未改业务逻辑;运行前请自行核对密钥、配额与隐私合规。

模板获取日期: 2026-10-24

步骤

  1. 解析并标准化 URL

    接收聊天输入后,用 Code 节点将原始文本按空格或逗号分割为数组,通过正则过滤有效 URL,自动补全 https:// 协议头,去重,并限制最多保留 10 个 URL。若输入无效则记录到 skipped_input。

    运行提示词 · 来源原文。Code 节点 'Parse & Normalise URLs' 的 jsCode 字段

    const rawInput = $input.first().json.chatInput || "";
    const rawUrls = rawInput.split(/[\s,]+/).filter(Boolean);
    
    let validUrls = [];
    let skipped = [];
    const urlRegex = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i;
    
    for (let url of rawUrls) {
      if (!urlRegex.test(url)) {
        skipped.push(url);
        continue;
      }
      let clean = url.toLowerCase();
      if (!clean.startsWith('http://') && !clean.startsWith('https://')) {
        clean = 'https://' + clean;
      }
      if (!validUrls.includes(clean)) {
        validUrls.push(clean);
      }
    }
    
    let warning = "";
    if (validUrls.length > 10) {
      warning = "⚠️ Only the first 10 URLs will be processed. Remaining URLs have been skipped.";
      validUrls = validUrls.slice(0, 10);
    }
    
    return [{
      json: {
        valid_urls: validUrls,
        skipped_input: skipped,
        warning: warning,
        total_valid: validUrls.length
      }
    }];
  2. 验证与分流

    检查解析后的 valid_urls 数量。若 total_valid 等于 0,通过 Chat Response 节点返回错误提示,要求用户重新输入格式正确的 URL;否则将 valid_urls 数组拆分为独立项目,进入下一步。

    运行提示词 · 来源原文。Code 节点 'Respond - No Valid URLs' 的 jsCode 字段

    return [{ json: { output: "❌ No valid URLs were found in your input. Please enter at least one properly formatted URL (e.g. https://example.com) and try again." } }];
  3. 可达性预检与过滤

    对每个有效 URL 执行 HTTP HEAD 请求检查在线状态。将状态码非 200 的 URL 聚合后通过聊天通知用户;状态码为 200 的 URL 拆分为独立项目,进入 PageSpeed API 循环。若全部 URL 均不可达,流程在此安全终止。

    运行提示词 · 来源原文。Code 节点 'Filter Reachable' 的 jsCode 字段

    // 1. Get all items from the current node (Reachability Check)
    const items = $input.all();
    
    // 2. Map through them to output only the two fields you want
    return items.map((item, index) => {
      
      // REACHBACK: Get the URL from the Split node using the index
      const splitNodeData = $('Split Valid URLs').all();
      const url = splitNodeData[index]?.json?.valid_urls || "URL Not Found";
      
      // DATA EXTRACTION: Pull status from the Reachability node
      const statusCode = item.json.statusCode || item.json.metadata?.statusCode;
    
      return {
        json: {
          valid_urls: url,
          status: statusCode
        }
      };
    });
  4. PageSpeed API 循环与数据提取

    通过 Loop 节点逐个处理可达 URL。每个 URL 向 Google PageSpeed Insights API (v5/runPagespeed) 发送 GET 请求,使用 mobile 策略,包含 performance/seo/best-practices/accessibility 四个类别。请求间插入 10 秒 Wait 节点控制速率。收到响应后,用 Code 节点解析 lighthouseResult,提取 Core Web Vitals、资源大小、节省建议等 40+ 字段,扁平化为表格行。

    运行提示词 · 来源原文。Code 节点 'Extract PageSpeed Data' 的 jsCode 字段

    const items = $input.all();
    let results = [];
    
    for (const entry of items) {
      const item = entry.json;
      const lh = item.lighthouseResult;
    
      // 1. RESILIENCE: Catch missing Lighthouse data
      if (!lh) {
        results.push({
          "Address": item.id || "Unknown URL",
          "PSI Request Status": "Error: No Lighthouse Data",
          "Performance Score": 0
        });
        continue;
      }
    
      const audits = lh.audits || {};
      const categories = lh.categories || {};
    
      // HELPER: Resource Summary
      const getRes = (type) => {
        const summary = audits['resource-summary'];
        const resItems = (summary && summary.details && summary.details.items) ? summary.details.items : [];
        const res = resItems.find(i => i.resourceType === type);
        return { size: res ? res.transferSize : 0, count: res ? res.requestCount : 0 };
      };
    
      // HELPER: Savings (Updated for v13 Insight Schema)
      const getSav = (id) => {
        const audit = audits[id];
        if (!audit) return { bytes: 0, ms: 0 };
        
        const details = audit.details || {};
        const metricSavings = audit.metricSavings || {};
        const debugData = details.debugData || {};
    
        // Logic to handle varying locations of savings data in modern audits
        const bytes = details.overallSavingsBytes || details.wastedBytes || debugData.wastedBytes || 0;
        const ms = details.overallSavingsMs || details.wastedMs || audit.numericValue || metricSavings.FCP || metricSavings.LCP || 0;
        
        return { bytes, ms };
      };
    
      // HELPER: Category Mapper
      const getCat = (score) => {
        if (score === null || score === undefined) return "N/A";
        if (score >= 0.9) return "Good";
        if (score >= 0.5) return "Needs Improvement";
        return "Poor";
      };
    
      // Pre-fetch nested insight objects to avoid long logical chains
      const docLatency = audits['document-latency-insight'];
      const lcpDiscovery = audits['lcp-discovery-insight'];
      const netTree = audits['network-dependency-tree-insight'];
      const thirdParties = audits['third-parties-insight'];
    
      results.push({
        "Address": lh.finalUrl || item.id,
        "PSI Request Status": "Success",
        "Performance Score": Math.round((categories.performance ? categories.performance.score : 0) * 100),
    
        // LAB METRICS
        "First Contentful Paint Time (ms)": audits['first-contentful-paint'] ? audits['first-contentful-paint'].numericValue : 0,
        "First Contentful Paint Score": Math.round((audits['first-contentful-paint'] ? audits['first-contentful-paint'].score : 0) * 100),
        "Speed Index Time (ms)": audits['speed-index'] ? audits['speed-index'].numericValue : 0,
        "Speed Index Score": Math.round((audits['speed-index'] ? audits['speed-index'].score : 0) * 100),
        "Largest Contentful Paint Time (ms)": audits['largest-contentful-paint'] ? audits['largest-contentful-paint'].numericValue : 0,
        "Largest Contentful Paint Score": Math.round((audits['largest-contentful-paint'] ? audits['largest-contentful-paint'].score : 0) * 100),
        "Time to Interactive (ms)": audits['interactive'] ? audits['interactive'].numericValue : 0,
        "Time to Interactive Score": Math.round((audits['interactive'] ? audits['interactive'].score : 0) * 100),
        "Max Potential First Input Delay (ms)": audits['max-potential-fid'] ? audits['max-potential-fid'].numericValue : 0,
        "Max Potential First Input Delay Score": Math.round((audits['max-potential-fid'] ? audits['max-potential-fid'].score : 0) * 100),
        "Total Blocking Time (ms)": audits['total-blocking-time'] ? audits['total-blocking-time'].numericValue : 0,
        "Total Blocking Time Score": Math.round((audits['total-blocking-time'] ? audits['total-blocking-time'].score : 0) * 100),
        "Cumulative Layout Shift": audits['cumulative-layout-shift'] ? audits['cumulative-layout-shift'].numericValue : 0,
        "Cumulative Layout Shift Score": Math.round((audits['cumulative-layout-shift'] ? audits['cumulative-layout-shift'].score : 0) * 100),
    
        // AGGREGATED SAVINGS
        "Total Size Savings (Bytes)": (getSav('unused-javascript').bytes + getSav('unused-css-rules').bytes),
        "Total Time Savings (ms)": getSav('render-blocking-insight').ms,
    
        // PAGE COMPOSITION
        "Total Requests": getRes('total').count,
        "Total Page Size (Bytes)": getRes('total').size,
        "HTML Size (Bytes)": getRes('document').size,
        "HTML Count": getRes('document').count,
        "Image Size (Bytes)": getRes('image').size,
        "Image Count": getRes('image').count,
        "CSS Size (Bytes)": getRes('stylesheet').size,
        "CSS Count": getRes('stylesheet').count,
        "JavaScript Size (Bytes)": getRes('script').size,
        "JavaScript Count": getRes('script').count,
        "Font Size (Bytes)": getRes('font').size,
        "Font Count": getRes('font').count,
        "Third Party Size (Bytes)": getRes('third-party').size,
        "Third Party Count": getRes('third-party').count,
    
        // GRANULAR SAVINGS
        "Minify CSS Savings (ms)": getSav('unminified-css').ms,
        "Minify CSS Savings (Bytes)": getSav('unminified-css').bytes,
        "Minify JavaScript Savings (ms)": getSav('unminified-javascript').ms,
        "Minify JavaScript Savings (Bytes)": getSav('unminified-javascript').bytes,
        "Reduce Unused CSS Savings (ms)": getSav('unused-css-rules').ms,
        "Reduce Unused CSS Savings (Bytes)": getSav('unused-css-rules').bytes,
        "Reduce Unused JavaScript Savings (ms)": getSav('unused-javascript').ms,
        "Reduce Unused JavaScript Savings (Bytes)": getSav('unused-javascript').bytes,
    
        // ADVANCED DIAGNOSTICS & INSIGHT MAPPINGS
        "JavaScript Execution Time (ms)": audits['bootup-time'] ? audits['bootup-time'].numericValue : 0,
        "JavaScript Execution Time Category": getCat(audits['bootup-time'] ? audits['bootup-time'].score : 0),
        "Minimize Main-Thread Work (ms)": audits['mainthread-work-breakdown'] ? audits['mainthread-work-breakdown'].numericValue : 0,
        "Minimize Main-Thread Work Category": getCat(audits['mainthread-work-breakdown'] ? audits['mainthread-work-breakdown'].score : 0),
        "Network Payload Size (Bytes)": audits['total-byte-weight'] ? audits['total-byte-weight'].numericValue : 0,
        "Accessibility Score": Math.round((categories.accessibility ? categories.accessibility.score : 0) * 100),
        "Server Responds Quickly": audits['server-response-time'] ? audits['server-response-time'].displayValue : "N/A",
        
        // Corrected logic for nested v13 Insight items
        "Applies Text Compression": (docLatency && docLatency.details && docLatency.details.items && docLatency.details.items.usesCompression && docLatency.details.items.usesCompression.value) ? "Yes" : "No",
        "LCP Request Discovery": (lcpDiscovery && lcpDiscovery.details && lcpDiscovery.details.items && lcpDiscovery.details.items.requestDiscoverable && lcpDiscovery.details.items.requestDiscoverable.value) ? "Optimized" : "Action Required",
        "LCP Breakdown": (audits['lcp-breakdown-insight'] && audits['lcp-breakdown-insight'].details && audits['lcp-breakdown-insight'].details.items ? audits['lcp-breakdown-insight'].details.items.length : 0) + " segments",
        
        "Render Blocking Requests Savings (ms)": getSav('render-blocking-insight').ms,
        "Preconnect Candidates Savings (ms)": getSav('uses-rel-preconnect').ms,
        "Maximum Critical Path Latency (ms)": (netTree && netTree.details && netTree.details.items && netTree.details.items.value && netTree.details.items.value.longestChain) ? netTree.details.items.value.longestChain.duration : 0,
        "Use Efficient Cache Lifetimes Savings (Bytes)": getSav('cache-insight').bytes,
        "Layout Shift Culprits": (audits['cls-culprits-insight'] && audits['cls-culprits-insight'].details && audits['cls-culprits-insight'].details.items) ? audits['cls-culprits-insight'].details.items.length : 0,
        "DOM Size": audits['dom-size-insight'] ? audits['dom-size-insight'].numericValue : 0,
        "Improve Image Delivery Savings (Bytes)": getSav('image-delivery-insight').bytes,
        "Forced Reflow Savings (ms)": getSav('forced-reflow-insight').ms,
        "Legacy JavaScript Savings (Bytes)": getSav('legacy-javascript-insight').bytes,
        "Duplicated JavaScript (Bytes)": getSav('duplicated-javascript-insight').bytes,
        "Font Display Savings (ms)": getSav('font-display-insight').ms,
        "3rd Parties": (thirdParties && thirdParties.details && thirdParties.details.items ? thirdParties.details.items.length : 0) + " resources found"
      });
    }
    
    return results.map(r => ({ json: r }));
  5. 生成 CSV 并上传

    将所有 URL 的扁平化数据通过 Convert to File 节点编译为 CSV 格式,然后以 multipart/form-data 方式 POST 到 uguu.se 获取临时下载链接,最后用 Code 节点从响应中提取 files[0].url。

    运行提示词 · 来源原文。Code 节点 'Extract Link' 的 jsCode 字段

    const url = $json.files?.[0]?.url;
    return [
      {
        json: {
          message: `<${url}>`
        }
      }
    ];
  6. 返回下载链接

    通过 Chat Response 节点向用户发送最终消息,包含 CSV 文件的临时下载链接。该节点设置为 executeOnce=true,确保只发送一次。

可复制提示词

搭建指导 · 本站建议版本。交给搭建助手,先确认输入和权限

这一段是提示词,页面不会在这里插入广告。

你正在搭建一个 n8n 工作流,用于批量获取网页的 Google PageSpeed Insights 性能指标并导出为 CSV。请先确认你已具备:1) 可访问的 n8n 实例;2) Google PageSpeed Insights API Key(在 n8n 凭证管理器中添加 httpQueryAuth 类型)。然后从 n8n 模板库导入工作流 15548,或按上述步骤手动配置节点。首次运行建议先用 1-2 个 URL 测试,确认凭证和节点连接正常后再处理完整列表。

首次运行前务必在 n8n 凭证管理器中配置 Google API Key,否则将触发 429 速率限制

来源

Export Google PageSpeed Insights data to CSV for multiple URLs · siddharth · n8n.io · 2026-05-08

留言与反馈

试过这个方法吗?分享效果、遇到的问题或改进建议。留言审核后公开,请勿填写敏感信息。

正在加载…

    相关用例