From 53bb540f70a9918ce74f7f27549a1c9f7fd7f6c3 Mon Sep 17 00:00:00 2001 From: Josef Strzibny Date: Tue, 1 Sep 2026 10:26:07 +0200 Subject: [PATCH 1/2] Add support for Markdown Output --- .gitignore | 1 + README.md | 66 ++++++++++++++++++++++++++++--------- README.md.erb | 66 ++++++++++++++++++++++++++++--------- serpapi.go | 64 +++++++++++++++++++++++++++++++---- test/markdown_test.go | 33 +++++++++++++++++++ test/search_archive_test.go | 10 ++++++ 6 files changed, 204 insertions(+), 36 deletions(-) create mode 100644 test/markdown_test.go diff --git a/.gitignore b/.gitignore index e43b0f9..be47843 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +.env diff --git a/README.md b/README.md index 9c181b9..748690d 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,29 @@ results, err := client.Search(parameter) fmt.Println(results) ``` -This example runs a search for "coffee" on Google. It then returns the results a Go map. -See the [playground](https://serpapi.com/playground) to generate your own code. +This example runs a search for "coffee" on Google and returns the results as a Go map. + +### Response formats + +Use `Search` for structured JSON results decoded into a Go map: + +```golang +results, err := client.Search(parameter) +``` + +Use `Markdown` for a token-efficient Markdown string optimized for LLMs and AI agents: + +```golang +markdown, err := client.Markdown(parameter) +``` + +Use `Html` when you need the raw search-engine response: + +```golang +rawHTML, err := client.Html(parameter) +``` + +Learn more about [SerpApi Markdown output](https://serpapi.com/markdown-output). See the [playground](https://serpapi.com/playground) to generate your own code. ## Advanced Usage ### Search API @@ -69,6 +90,13 @@ func main() { } fmt.Println(rsp) + // token-efficient Markdown as a string + markdown, err := client.Markdown(parameter) + if err != nil { + panic(err) + } + fmt.Println(*markdown) + // raw search engine html as a String // serpapi.com acts a proxy to provive high throughputs, no search limit and more. raw_html, err := client.Html(parameter) @@ -121,11 +149,9 @@ To fetch earlier results from the search_id. First, you need to run a search and save the search id. ```golang // First, you need to run a search and save the search id. -auth := map[string]string{ - "engine": "google", - "api_key": "secret_api_key", -} -client := serpapi.NewClient(auth) +setting := serpapi.NewSerpApiClientSetting("secret_api_key") +setting.Engine = "google" +client := serpapi.NewClient(setting) parameter := map[string]string{ "q": "Coffee", "location": "Portland"} @@ -133,30 +159,35 @@ parameter := map[string]string{ rsp, err := client.Search(parameter) if err != nil { - t.Error("unexpected error", err) - return + panic(err) } // Now let's retrieve the previous search results from the archive. searchID := rsp["search_metadata"].(map[string]interface{})["id"].(string) if len(searchID) == 0 { - t.Error("search_metadata.id must be defined") - return + panic("search_metadata.id must be defined") } searchArchive, err := client.SearchArchive(searchID) if err != nil { - t.Error(err) - return + panic(err) } searchIDArchive := searchArchive["search_metadata"].(map[string]interface{})["id"].(string) if searchIDArchive != searchID { - t.Error("search_metadata.id do not match", searchIDArchive, searchID) + panic("search_metadata.id does not match") } ``` -This code prints the search results from the archive. :) +This code prints the JSON search results from the archive. Archived results are also available as Markdown: + +```golang +markdown, err := client.SearchArchiveMarkdown(searchID) +if err != nil { + panic(err) +} +fmt.Println(*markdown) +``` ### Account API ```golang @@ -2005,6 +2036,9 @@ Go versions validated by Github Actions: * see: [Github Actions.](https://github.com/serpapi/serpapi-golang/actions/workflows/ci.yml) ## Change logs + * [2026-09-01] 1.2.0 Markdown Output Support + - Add Markdown search and archive responses + - Decode JSON API errors returned for Markdown requests * [2026-01-26] 1.1.0 Asynchronous & Persistent Mode Support - Major features (async/persistent mode, API key handling, client configuration) - New test examples @@ -2046,9 +2080,11 @@ classDiagram api_key String params Map search() Map + markdown() String html() String location() String search_archive() Map + search_archive_markdown() String account() Map } net/http <.. Client diff --git a/README.md.erb b/README.md.erb index 5cdd5c6..4008503 100644 --- a/README.md.erb +++ b/README.md.erb @@ -52,8 +52,29 @@ results, err := client.Search(parameter) fmt.Println(results) ``` -This example runs a search for "coffee" on Google. It then returns the results a Go map. -See the [playground](https://serpapi.com/playground) to generate your own code. +This example runs a search for "coffee" on Google and returns the results as a Go map. + +### Response formats + +Use `Search` for structured JSON results decoded into a Go map: + +```golang +results, err := client.Search(parameter) +``` + +Use `Markdown` for a token-efficient Markdown string optimized for LLMs and AI agents: + +```golang +markdown, err := client.Markdown(parameter) +``` + +Use `Html` when you need the raw search-engine response: + +```golang +rawHTML, err := client.Html(parameter) +``` + +Learn more about [SerpApi Markdown output](https://serpapi.com/markdown-output). See the [playground](https://serpapi.com/playground) to generate your own code. ## Advanced Usage ### Search API @@ -92,6 +113,13 @@ func main() { } fmt.Println(rsp) + // token-efficient Markdown as a string + markdown, err := client.Markdown(parameter) + if err != nil { + panic(err) + } + fmt.Println(*markdown) + // raw search engine html as a String // serpapi.com acts a proxy to provive high throughputs, no search limit and more. raw_html, err := client.Html(parameter) @@ -144,11 +172,9 @@ To fetch earlier results from the search_id. First, you need to run a search and save the search id. ```golang // First, you need to run a search and save the search id. -auth := map[string]string{ - "engine": "google", - "api_key": "secret_api_key", -} -client := serpapi.NewClient(auth) +setting := serpapi.NewSerpApiClientSetting("secret_api_key") +setting.Engine = "google" +client := serpapi.NewClient(setting) parameter := map[string]string{ "q": "Coffee", "location": "Portland"} @@ -156,30 +182,35 @@ parameter := map[string]string{ rsp, err := client.Search(parameter) if err != nil { - t.Error("unexpected error", err) - return + panic(err) } // Now let's retrieve the previous search results from the archive. searchID := rsp["search_metadata"].(map[string]interface{})["id"].(string) if len(searchID) == 0 { - t.Error("search_metadata.id must be defined") - return + panic("search_metadata.id must be defined") } searchArchive, err := client.SearchArchive(searchID) if err != nil { - t.Error(err) - return + panic(err) } searchIDArchive := searchArchive["search_metadata"].(map[string]interface{})["id"].(string) if searchIDArchive != searchID { - t.Error("search_metadata.id do not match", searchIDArchive, searchID) + panic("search_metadata.id does not match") } ``` -This code prints the search results from the archive. :) +This code prints the JSON search results from the archive. Archived results are also available as Markdown: + +```golang +markdown, err := client.SearchArchiveMarkdown(searchID) +if err != nil { + panic(err) +} +fmt.Println(*markdown) +``` ### Account API ```golang @@ -366,6 +397,9 @@ Go versions validated by Github Actions: * see: [Github Actions.](https://github.com/serpapi/serpapi-golang/actions/workflows/ci.yml) ## Change logs + * [2026-09-01] 1.2.0 Markdown Output Support + - Add Markdown search and archive responses + - Decode JSON API errors returned for Markdown requests * [2026-01-26] 1.1.0 Asynchronous & Persistent Mode Support - Major features (async/persistent mode, API key handling, client configuration) - New test examples @@ -407,9 +441,11 @@ classDiagram api_key String params Map search() Map + markdown() String html() String location() String search_archive() Map + search_archive_markdown() String account() Map } net/http <.. Client diff --git a/serpapi.go b/serpapi.go index 99e94b4..78d08b8 100644 --- a/serpapi.go +++ b/serpapi.go @@ -5,13 +5,15 @@ import ( "errors" "fmt" "io" + "mime" "net/http" "net/url" + "strings" "time" ) const ( - VERSION = "1.1.0" + VERSION = "1.2.0" BaseURL = "https://serpapi.com" DefaultTimeout = 60 * time.Second ) @@ -79,7 +81,17 @@ func (client *SerpApiClient) Html(parameter map[string]string) (*string, error) return nil, err } defer rsp.Body.Close() - return client.decodeHTML(rsp.Body) + return client.decodeText(rsp.Body) +} + +// Markdown returns search results formatted as Markdown +func (client *SerpApiClient) Markdown(parameter map[string]string) (*string, error) { + rsp, err := client.execute("/search", "md", parameter) + if err != nil { + return nil, err + } + defer rsp.Body.Close() + return client.decodeTextResponse(rsp) } // Location returns standardized location data @@ -116,6 +128,16 @@ func (client *SerpApiClient) SearchArchive(id string) (map[string]interface{}, e return client.decodeJSON(rsp.Body) } +// SearchArchiveMarkdown retrieves previous search results formatted as Markdown +func (client *SerpApiClient) SearchArchiveMarkdown(id string) (*string, error) { + rsp, err := client.execute("/searches/"+id+".md", "md", map[string]string{}) + if err != nil { + return nil, err + } + defer rsp.Body.Close() + return client.decodeTextResponse(rsp) +} + // decodeJSON decodes response body to a map func (client *SerpApiClient) decodeJSON(body io.ReadCloser) (map[string]interface{}, error) { defer body.Close() @@ -141,9 +163,8 @@ func (client *SerpApiClient) decodeJSONArray(body io.ReadCloser) ([]interface{}, return rsp, nil } -// decodeHTML decodes response body to an HTML string -func (client *SerpApiClient) decodeHTML(body io.ReadCloser) (*string, error) { - defer body.Close() +// decodeText decodes a response body to a string. +func (client *SerpApiClient) decodeText(body io.Reader) (*string, error) { buffer, err := io.ReadAll(body) if err != nil { return nil, err @@ -152,6 +173,37 @@ func (client *SerpApiClient) decodeHTML(body io.ReadCloser) (*string, error) { return &text, nil } +// decodeTextResponse decodes Markdown responses to a string. SerpApi returns +// API errors as JSON even when Markdown output was requested. +func (client *SerpApiClient) decodeTextResponse(rsp *http.Response) (*string, error) { + buffer, err := io.ReadAll(rsp.Body) + if err != nil { + return nil, err + } + + mediaType, _, _ := mime.ParseMediaType(rsp.Header.Get("Content-Type")) + mediaType = strings.ToLower(mediaType) + isJSON := mediaType == "application/json" || strings.HasSuffix(mediaType, "+json") + if isJSON { + var data map[string]interface{} + if err := json.Unmarshal(buffer, &data); err == nil { + if errorMessage, exists := data["error"].(string); exists { + return nil, errors.New(errorMessage) + } + } + } + + if rsp.StatusCode < http.StatusOK || rsp.StatusCode >= http.StatusMultipleChoices { + return nil, fmt.Errorf("HTTP request failed with status: %d", rsp.StatusCode) + } + if isJSON { + return nil, errors.New("expected a Markdown response, received JSON") + } + + text := string(buffer) + return &text, nil +} + // execute sends an HTTP GET request and returns the response func (client *SerpApiClient) execute(path string, output string, parameter map[string]string) (*http.Response, error) { query := url.Values{} @@ -177,7 +229,7 @@ func (client *SerpApiClient) execute(path string, output string, parameter map[s query.Add("async", "true") } query.Add("source", "go:"+VERSION) - query.Add("output", output) + query.Set("output", output) endpoint := BaseURL + path + "?" + query.Encode() rsp, err := client.HttpSearch.Get(endpoint) diff --git a/test/markdown_test.go b/test/markdown_test.go new file mode 100644 index 0000000..9209f76 --- /dev/null +++ b/test/markdown_test.go @@ -0,0 +1,33 @@ +package serpapi + +import ( + "strings" + "testing" + + "github.com/serpapi/serpapi-golang" +) + +func TestMarkdown(t *testing.T) { + if shoulSkip() { + t.Skip("SERPAPI_KEY required") + return + } + + setting := serpapi.NewSerpApiClientSetting(getApiKey()) + setting.Engine = "google" + client := serpapi.NewClient(setting) + + markdown, err := client.Markdown(map[string]string{ + "q": "Coffee", + "location": "Austin, Texas, United States", + }) + if err != nil { + t.Fatalf("Markdown returned an error: %v", err) + } + if !strings.HasPrefix(*markdown, "---") { + t.Error("Markdown response does not contain YAML frontmatter") + } + if !strings.Contains(*markdown, "## Organic Results") { + t.Error("Markdown response does not contain organic results") + } +} diff --git a/test/search_archive_test.go b/test/search_archive_test.go index 13d295d..d1dbfe2 100644 --- a/test/search_archive_test.go +++ b/test/search_archive_test.go @@ -1,6 +1,7 @@ package serpapi import ( + "strings" "testing" "github.com/serpapi/serpapi-golang" @@ -73,5 +74,14 @@ func TestSearchArchive(t *testing.T) { if !ok || searchIDArchive != searchID { t.Errorf("search_metadata.id mismatch: got %v, expected %v", searchIDArchive, searchID) } + + markdown, err := client.SearchArchiveMarkdown(searchID) + if err != nil { + t.Error(err) + return + } + if !strings.Contains(*markdown, "## Organic Results") { + t.Error("Markdown search archive does not contain organic results") + } // print search results from search archive } From c598541b07fbe11fdda68a873be6b4f718b486b5 Mon Sep 17 00:00:00 2001 From: Josef Strzibny Date: Tue, 1 Sep 2026 15:35:48 +0200 Subject: [PATCH 2/2] Prefer Md to Markdown --- README.md | 12 ++++++------ README.md.erb | 12 ++++++------ serpapi.go | 8 ++++---- test/markdown_test.go | 2 +- test/search_archive_test.go | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 748690d..e6e0470 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,10 @@ Use `Search` for structured JSON results decoded into a Go map: results, err := client.Search(parameter) ``` -Use `Markdown` for a token-efficient Markdown string optimized for LLMs and AI agents: +Use `Md` for a token-efficient Markdown string optimized for LLMs and AI agents: ```golang -markdown, err := client.Markdown(parameter) +markdown, err := client.Md(parameter) ``` Use `Html` when you need the raw search-engine response: @@ -91,7 +91,7 @@ func main() { fmt.Println(rsp) // token-efficient Markdown as a string - markdown, err := client.Markdown(parameter) + markdown, err := client.Md(parameter) if err != nil { panic(err) } @@ -182,7 +182,7 @@ if searchIDArchive != searchID { This code prints the JSON search results from the archive. Archived results are also available as Markdown: ```golang -markdown, err := client.SearchArchiveMarkdown(searchID) +markdown, err := client.SearchArchiveMd(searchID) if err != nil { panic(err) } @@ -2080,11 +2080,11 @@ classDiagram api_key String params Map search() Map - markdown() String + md() String html() String location() String search_archive() Map - search_archive_markdown() String + search_archive_md() String account() Map } net/http <.. Client diff --git a/README.md.erb b/README.md.erb index 4008503..33c8b9c 100644 --- a/README.md.erb +++ b/README.md.erb @@ -62,10 +62,10 @@ Use `Search` for structured JSON results decoded into a Go map: results, err := client.Search(parameter) ``` -Use `Markdown` for a token-efficient Markdown string optimized for LLMs and AI agents: +Use `Md` for a token-efficient Markdown string optimized for LLMs and AI agents: ```golang -markdown, err := client.Markdown(parameter) +markdown, err := client.Md(parameter) ``` Use `Html` when you need the raw search-engine response: @@ -114,7 +114,7 @@ func main() { fmt.Println(rsp) // token-efficient Markdown as a string - markdown, err := client.Markdown(parameter) + markdown, err := client.Md(parameter) if err != nil { panic(err) } @@ -205,7 +205,7 @@ if searchIDArchive != searchID { This code prints the JSON search results from the archive. Archived results are also available as Markdown: ```golang -markdown, err := client.SearchArchiveMarkdown(searchID) +markdown, err := client.SearchArchiveMd(searchID) if err != nil { panic(err) } @@ -441,11 +441,11 @@ classDiagram api_key String params Map search() Map - markdown() String + md() String html() String location() String search_archive() Map - search_archive_markdown() String + search_archive_md() String account() Map } net/http <.. Client diff --git a/serpapi.go b/serpapi.go index 78d08b8..00d6f09 100644 --- a/serpapi.go +++ b/serpapi.go @@ -84,8 +84,8 @@ func (client *SerpApiClient) Html(parameter map[string]string) (*string, error) return client.decodeText(rsp.Body) } -// Markdown returns search results formatted as Markdown -func (client *SerpApiClient) Markdown(parameter map[string]string) (*string, error) { +// Md returns search results formatted as Markdown +func (client *SerpApiClient) Md(parameter map[string]string) (*string, error) { rsp, err := client.execute("/search", "md", parameter) if err != nil { return nil, err @@ -128,8 +128,8 @@ func (client *SerpApiClient) SearchArchive(id string) (map[string]interface{}, e return client.decodeJSON(rsp.Body) } -// SearchArchiveMarkdown retrieves previous search results formatted as Markdown -func (client *SerpApiClient) SearchArchiveMarkdown(id string) (*string, error) { +// SearchArchiveMd retrieves previous search results formatted as Markdown +func (client *SerpApiClient) SearchArchiveMd(id string) (*string, error) { rsp, err := client.execute("/searches/"+id+".md", "md", map[string]string{}) if err != nil { return nil, err diff --git a/test/markdown_test.go b/test/markdown_test.go index 9209f76..7ce8196 100644 --- a/test/markdown_test.go +++ b/test/markdown_test.go @@ -17,7 +17,7 @@ func TestMarkdown(t *testing.T) { setting.Engine = "google" client := serpapi.NewClient(setting) - markdown, err := client.Markdown(map[string]string{ + markdown, err := client.Md(map[string]string{ "q": "Coffee", "location": "Austin, Texas, United States", }) diff --git a/test/search_archive_test.go b/test/search_archive_test.go index d1dbfe2..4e99788 100644 --- a/test/search_archive_test.go +++ b/test/search_archive_test.go @@ -75,7 +75,7 @@ func TestSearchArchive(t *testing.T) { t.Errorf("search_metadata.id mismatch: got %v, expected %v", searchIDArchive, searchID) } - markdown, err := client.SearchArchiveMarkdown(searchID) + markdown, err := client.SearchArchiveMd(searchID) if err != nil { t.Error(err) return