fish2018/pansou · error
[ ] 未能在首页找到 data-key
Error message
[%s] 未能在首页找到 data-key
What it means
Returned when the homepage HTML parsed successfully but no <script> tag content matches dataKeyRegex, so the plugin cannot extract the data-key value needed to call the Ghost Content API. This indicates the site's page structure changed or the served page no longer embeds the expected data-key attribute.
Solutions
- Open https://jsnoteclub.com/ in a browser, view source, and locate where data-key now appears
- Update dataKeyRegex to match the new attribute/markup pattern
- If the key moved out of script tags, search the full document HTML (doc.Find("[data-key]")) instead of only script elements
- Check whether the Ghost Content API key is published in a JS bundle or meta tag now and adjust extraction
- Fallback: configure the data-key statically via plugin config instead of scraping
Example fix
// before
match := dataKeyRegex.FindStringSubmatch(htmlBuilder.String())
if len(match) < 2 {
return "", fmt.Errorf("[%s] 未能在首页找到 data-key", p.Name())
}
// after
match := dataKeyRegex.FindStringSubmatch(htmlBuilder.String())
if len(match) < 2 {
// fallback: look for data-key attribute anywhere in the document
if sel := doc.Find("[data-key]"); sel.Length() > 0 {
if key, ok := sel.Attr("data-key"); ok && key != "" {
return key, nil
}
}
return "", fmt.Errorf("[%s] 未能在首页找到 data-key", p.Name())
} Defensive patterns
Strategy: fallback
Validate before calling
// check the site still embeds the key before running the pipeline
resp, err := http.Get("https://jsnoteclub.com/")
if err != nil { /* network problem, different error */ }
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), "data-key") && !dataKeyRegex.MatchString(string(body)) {
// upstream layout changed: alert/maintain config-supplied key instead
} Try / catch
key, err := plugin.fetchDataKey(client)
if err != nil {
if strings.Contains(err.Error(), "未能") {
// selector rot: fall back to a configured static key
return useConfiguredDataKey()
}
return err
} Prevention
- Add a CI smoke test that scrapes the homepage and asserts the regex still matches
- Pin a configurable fallback data-key so scraping failures are non-fatal
- Alert on this error — it almost always means the upstream site changed its markup
- Extract the key from data-key attributes anywhere in the document, not only <script> tags
When it happens
Trigger: The regex finds no match (len(match) < 2) after scanning the outer HTML of every <script> element in the homepage document — i.e. no script tag contains the data-key pattern the regex expects.
Common situations: jsnoteclub.com redesigned its frontend and the data-key is no longer in a <script> tag; the Ghost site switched themes so the key moved to a different attribute; the plugin scrapes a JS-rendered shell where content loads via XHR; site now serves a maintenance or consent page instead of the app HTML.
Related errors
AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07).
Data as JSON: /api/errors/79e0b08119e7928f.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/jsnoteclub/jsnoteclub.go:258
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("[%s] 首页返回状态码: %d", p.Name(), resp.StatusCode)
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return "", fmt.Errorf("[%s] 解析首页失败: %w", p.Name(), err)
}
var htmlBuilder strings.Builder
doc.Find("script").Each(func(_ int, s *goquery.Selection) {
if html, err := goquery.OuterHtml(s); err == nil {
htmlBuilder.WriteString(html)
}
})
match := dataKeyRegex.FindStringSubmatch(htmlBuilder.String())
if len(match) < 2 {
return "", fmt.Errorf("[%s] 未能在首页找到 data-key", p.Name())
}
return match[1], nil
}
func (p *JsNoteClubPlugin) fetchPosts(client *http.Client, dataKey string) ([]ghostPost, error) {
params := url.Values{}
params.Set("key", dataKey)
params.Set("limit", "10000")
params.Set("fields", "id,slug,title,excerpt,url,updated_at,visibility")
params.Set("order", "updated_at DESC")
reqURL := fmt.Sprintf("https://jsnoteclub.com/ghost/api/content/posts/?%s", params.Encode())
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)View on GitHub (pinned to beaa561337)