fix(frameworks): stop ghost detector matching the generic ghost-button (#163)

the ghost detector keyed on the bare "ghost-" body substring, which fires on
any page using the common "ghost-button" or skeleton-loader CSS class (e.g. a
WordPress theme), scoring 0.65 and reporting it as Ghost CMS. key on the
canonical `<meta name="generator" content="Ghost` tag instead, matching the
existing astro detector. the /ghost/api/ path and Ghost header markers are
unchanged.

a ghost site that emits neither the generator meta nor a /ghost/api/
reference is no longer detected; the Ghost response header alone scores
below the detection floor.
This commit is contained in:
Tigah
2026-06-22 16:47:43 -07:00
committed by GitHub
parent 291846dde5
commit bca4831df1
2 changed files with 80 additions and 1 deletions
+79
View File
@@ -568,6 +568,85 @@ func TestDetectFramework_Astro(t *testing.T) {
}
}
func TestDetectFramework_Ghost(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<meta name="generator" content="Ghost 6.46">
</head>
<body>Content</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Ghost" {
t.Errorf("expected framework 'Ghost', got '%s'", result.Name)
}
}
// ghost-button is a common generic CSS class and must not read as Ghost CMS.
func TestDetectFramework_GhostButtonNoMatch(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<body>
<a class="ghost-button" href="/signup">Sign up</a>
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != nil && result.Name == "Ghost" {
t.Errorf("expected no Ghost detection for a ghost-button page, got confidence %.2f", result.Confidence)
}
}
// the /ghost/api/ path is the only Ghost marker left for pages without the
// generator meta, so guard that it still detects on its own.
func TestDetectFramework_GhostAPIPath(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<body>
<script src="/ghost/api/content/posts/?key=abc"></script>
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Ghost" {
t.Errorf("expected framework 'Ghost', got '%s'", result.Name)
}
}
func TestExtractVersion_Astro(t *testing.T) {
tests := []struct {
body string
+1 -1
View File
@@ -173,7 +173,7 @@ func (d *ghostDetector) Name() string { return "Ghost" }
func (d *ghostDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "ghost-", Weight: 0.4},
{Pattern: `<meta name="generator" content="Ghost`, Weight: 0.4},
{Pattern: "Ghost", Weight: 0.3, HeaderOnly: true},
{Pattern: "/ghost/api/", Weight: 0.4},
}