From 7df37b9c4a4c0cbadbc23be66b403983af1db506 Mon Sep 17 00:00:00 2001 From: Josef Strzibny Date: Mon, 17 Aug 2026 14:05:08 +0200 Subject: [PATCH 1/3] Add initial Markdown support --- README.md | 28 +++++++++++++++ README.md.erb | 28 +++++++++++++++ src/Client.php | 25 ++++++++++---- tests/ClientIntegrationTest.php | 8 +++++ tests/ClientTest.php | 61 ++++++++++++++++++++++++++++++++- 5 files changed, 142 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 41abac3..d5a065e 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,30 @@ This example runs a search for "coffee" on Google. It returns the results as a P See the [playground](https://serpapi.com/playground) to generate your own code. +## Response formats + +Use `search` for structured results decoded into a PHP object: + +```php +$results = $client->search(['q' => 'coffee']); +``` + +Use `md` for a token-efficient Markdown string optimized for LLMs and AI agents: + +```php +$markdown = $client->md(['q' => 'coffee']); +``` + +Use `html` when you need the raw search-engine response: + +```php +$html = $client->html(['q' => 'coffee']); +``` + +Archived results are also available as Markdown with `$client->searchArchive($searchId, 'md')`. + +Learn more about [SerpApi Markdown output](https://serpapi.com/markdown-output). + ## Configuration ### API key @@ -468,6 +492,9 @@ Now retrieve the previous search from the archive (free of charge): ```php $archived = $client->searchArchive($search_id); print_r($archived); + +$markdown = $client->searchArchive($search_id, 'md'); +echo $markdown; ``` ### Account API @@ -538,6 +565,7 @@ Contributions are welcome. Feel free to submit a pull request! ## Change log + * Unreleased - Add Markdown search and archive output support * 1.0 - First stable version ## Conclusion diff --git a/README.md.erb b/README.md.erb index 389c0b0..269a965 100644 --- a/README.md.erb +++ b/README.md.erb @@ -86,6 +86,30 @@ This example runs a search for "coffee" on Google. It returns the results as a P See the [playground](https://serpapi.com/playground) to generate your own code. +## Response formats + +Use `search` for structured results decoded into a PHP object: + +```php +$results = $client->search(['q' => 'coffee']); +``` + +Use `md` for a token-efficient Markdown string optimized for LLMs and AI agents: + +```php +$markdown = $client->md(['q' => 'coffee']); +``` + +Use `html` when you need the raw search-engine response: + +```php +$html = $client->html(['q' => 'coffee']); +``` + +Archived results are also available as Markdown with `$client->searchArchive($searchId, 'md')`. + +Learn more about [SerpApi Markdown output](https://serpapi.com/markdown-output). + ## Configuration ### API key @@ -244,6 +268,9 @@ Now retrieve the previous search from the archive (free of charge): ```php $archived = $client->searchArchive($search_id); print_r($archived); + +$markdown = $client->searchArchive($search_id, 'md'); +echo $markdown; ``` ### Account API @@ -314,6 +341,7 @@ Contributions are welcome. Feel free to submit a pull request! ## Change log + * Unreleased - Add Markdown search and archive output support * 1.0 - First stable version ## Conclusion diff --git a/src/Client.php b/src/Client.php index 2d4b034..0891b57 100644 --- a/src/Client.php +++ b/src/Client.php @@ -76,6 +76,17 @@ public function search(array $params = []): object return $this->get('/search', 'json', $params); } + /** + * Run a search and return Markdown optimized for LLMs and AI agents. + * + * @param array $params + * @throws SerpApiException + */ + public function md(array $params = []): string + { + return $this->get('/search.md', 'md', $params); + } + /** * Run a search and return raw HTML. * @@ -122,8 +133,8 @@ public function searchArchive(string $searchId, string $format = 'json') throw new SerpApiException('search_id must be present'); } - if (!in_array($format, ['json', 'html'], true)) { - throw new SerpApiException('format must be json or html'); + if (!in_array($format, ['json', 'html', 'md'], true)) { + throw new SerpApiException('format must be json, html, or md'); } $safeSearchId = rawurlencode($searchId); @@ -137,8 +148,8 @@ public function searchArchive(string $searchId, string $format = 'json') */ private function get(string $endpoint, string $format = 'json', array $params = []) { - if (!in_array($format, ['json', 'html'], true)) { - throw new SerpApiException("Unsupported format '$format'. Expected 'html' or 'json'."); + if (!in_array($format, ['json', 'html', 'md'], true)) { + throw new SerpApiException("Unsupported format '$format'. Expected 'html', 'json', or 'md'."); } $apiKey = $params['api_key'] ?? $this->apiKey; @@ -171,12 +182,12 @@ private function get(string $endpoint, string $format = 'json', array $params = throw new SerpApiException('cURL error: ' . $curlError); } - if ($format === 'html') { + if (in_array($format, ['html', 'md'], true)) { if ($httpCode === 200) { return $response; } - $this->raiseHttpError($httpCode, $endpoint, $query, null, null, 'html'); + $this->raiseHttpError($httpCode, $endpoint, $query, null, null, $format); } $decoded = json_decode($response); @@ -204,7 +215,7 @@ private function get(string $endpoint, string $format = 'json', array $params = * @return array{response: string|false, http_code: int, curl_error: string} * @throws SerpApiException */ - private function request(string $url): array + protected function request(string $url): array { $ch = curl_init(); if ($ch === false) { diff --git a/tests/ClientIntegrationTest.php b/tests/ClientIntegrationTest.php index 2acd6db..b3aee88 100644 --- a/tests/ClientIntegrationTest.php +++ b/tests/ClientIntegrationTest.php @@ -30,6 +30,14 @@ public function testHtml() $this->assertGreaterThan(10000, strlen($response)); } + public function testMd() + { + $client = $this->serpApiClient(); + $response = $client->md($this->searchParams); + $this->assertStringStartsWith('---', $response); + $this->assertStringContainsString('## Organic Results', $response); + } + public function testSearch() { $client = $this->serpApiClient(); diff --git a/tests/ClientTest.php b/tests/ClientTest.php index c2f3920..24d2180 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -46,11 +46,70 @@ public function testSearchArchiveThrowsWhenIdEmpty() public function testSearchArchiveThrowsWhenFormatInvalid() { $this->expectException(SerpApiException::class); - $this->expectExceptionMessage('format must be json or html'); + $this->expectExceptionMessage('format must be json, html, or md'); $client = new Client('test_key'); $client->searchArchive('abc', 'xml'); } + public function testMdReturnsRawMarkdown() + { + $markdown = "---\nengine: google\n---\n\n## Organic Results\n"; + $client = new StubClient([ + 'response' => $markdown, + 'http_code' => 200, + 'curl_error' => '', + ]); + + $response = $client->md(['q' => 'Coffee']); + $request = $client->getLastRequest(); + $query = []; + parse_str($request['query'], $query); + + $this->assertSame($markdown, $response); + $this->assertSame('/search.md', $request['path']); + $this->assertSame('md', $query['output']); + $this->assertSame('Coffee', $query['q']); + } + + public function testSearchArchiveReturnsRawMarkdown() + { + $markdown = "---\nengine: google\n---\n"; + $client = new StubClient([ + 'response' => $markdown, + 'http_code' => 200, + 'curl_error' => '', + ]); + + $response = $client->searchArchive('search/id', 'md'); + $request = $client->getLastRequest(); + + $this->assertSame($markdown, $response); + $this->assertSame('/searches/search%2Fid.md', $request['path']); + } + + public function testMdHttpErrorUsesMdDecoder() + { + $client = new StubClient([ + 'response' => 'Invalid search', + 'http_code' => 400, + 'curl_error' => '', + ]); + + try { + $client->md(['q' => 'Coffee']); + $this->fail('Expected SerpApiException was not thrown'); + } catch (SerpApiException $exception) { + $this->assertSame(400, $exception->getResponseStatus()); + $this->assertSame('md', $exception->getDecoder()); + $this->assertSame([ + 'engine' => 'google', + 'source' => 'php', + 'q' => 'Coffee', + 'output' => 'md', + ], $exception->getSearchParams()); + } + } + public function testSetApiKeyThrowsWhenEmpty() { $this->expectException(SerpApiException::class); From 7b0553f986a2d55399e402bc763db341b5375619 Mon Sep 17 00:00:00 2001 From: Josef Strzibny Date: Mon, 17 Aug 2026 17:21:57 +0200 Subject: [PATCH 2/3] Keep tests live for now --- src/Client.php | 2 +- tests/ClientIntegrationTest.php | 4 +++ tests/ClientTest.php | 59 --------------------------------- 3 files changed, 5 insertions(+), 60 deletions(-) diff --git a/src/Client.php b/src/Client.php index 0891b57..156b83c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -215,7 +215,7 @@ private function get(string $endpoint, string $format = 'json', array $params = * @return array{response: string|false, http_code: int, curl_error: string} * @throws SerpApiException */ - protected function request(string $url): array + private function request(string $url): array { $ch = curl_init(); if ($ch === false) { diff --git a/tests/ClientIntegrationTest.php b/tests/ClientIntegrationTest.php index b3aee88..6c73272 100644 --- a/tests/ClientIntegrationTest.php +++ b/tests/ClientIntegrationTest.php @@ -62,5 +62,9 @@ public function testSearchArchive() $result = $client->search($this->searchParams); $archived_result = $client->searchArchive($result->search_metadata->id); $this->assertEquals($result->search_metadata->id, $archived_result->search_metadata->id); + + $archived_markdown = $client->searchArchive($result->search_metadata->id, 'md'); + $this->assertStringStartsWith('---', $archived_markdown); + $this->assertStringContainsString('## Organic Results', $archived_markdown); } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 24d2180..4cebd51 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -51,65 +51,6 @@ public function testSearchArchiveThrowsWhenFormatInvalid() $client->searchArchive('abc', 'xml'); } - public function testMdReturnsRawMarkdown() - { - $markdown = "---\nengine: google\n---\n\n## Organic Results\n"; - $client = new StubClient([ - 'response' => $markdown, - 'http_code' => 200, - 'curl_error' => '', - ]); - - $response = $client->md(['q' => 'Coffee']); - $request = $client->getLastRequest(); - $query = []; - parse_str($request['query'], $query); - - $this->assertSame($markdown, $response); - $this->assertSame('/search.md', $request['path']); - $this->assertSame('md', $query['output']); - $this->assertSame('Coffee', $query['q']); - } - - public function testSearchArchiveReturnsRawMarkdown() - { - $markdown = "---\nengine: google\n---\n"; - $client = new StubClient([ - 'response' => $markdown, - 'http_code' => 200, - 'curl_error' => '', - ]); - - $response = $client->searchArchive('search/id', 'md'); - $request = $client->getLastRequest(); - - $this->assertSame($markdown, $response); - $this->assertSame('/searches/search%2Fid.md', $request['path']); - } - - public function testMdHttpErrorUsesMdDecoder() - { - $client = new StubClient([ - 'response' => 'Invalid search', - 'http_code' => 400, - 'curl_error' => '', - ]); - - try { - $client->md(['q' => 'Coffee']); - $this->fail('Expected SerpApiException was not thrown'); - } catch (SerpApiException $exception) { - $this->assertSame(400, $exception->getResponseStatus()); - $this->assertSame('md', $exception->getDecoder()); - $this->assertSame([ - 'engine' => 'google', - 'source' => 'php', - 'q' => 'Coffee', - 'output' => 'md', - ], $exception->getSearchParams()); - } - } - public function testSetApiKeyThrowsWhenEmpty() { $this->expectException(SerpApiException::class); From 2173d5bcb5ae4e40ad974d240869d89844465585 Mon Sep 17 00:00:00 2001 From: Josef Strzibny Date: Mon, 17 Aug 2026 17:24:04 +0200 Subject: [PATCH 3/3] Remove Markdown line from changelog --- README.md | 1 - README.md.erb | 1 - 2 files changed, 2 deletions(-) diff --git a/README.md b/README.md index d5a065e..586dcb0 100644 --- a/README.md +++ b/README.md @@ -565,7 +565,6 @@ Contributions are welcome. Feel free to submit a pull request! ## Change log - * Unreleased - Add Markdown search and archive output support * 1.0 - First stable version ## Conclusion diff --git a/README.md.erb b/README.md.erb index 269a965..54e3ba5 100644 --- a/README.md.erb +++ b/README.md.erb @@ -341,7 +341,6 @@ Contributions are welcome. Feel free to submit a pull request! ## Change log - * Unreleased - Add Markdown search and archive output support * 1.0 - First stable version ## Conclusion