diff --git a/README.md b/README.md index 94ffb83..cf1da5e 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,9 @@ Once the server is running, the interactive docs are available locally: To browse the spec without running the server, see [docs/openapi.yaml](./docs/openapi.yaml). For a higher-level overview of how OpenSERP works internally, see the [architecture docs](https://openserp.org/docs/architecture/). -## SDKs & Integrations +## SDKs & Examples -Official client packages. Each works against your self-hosted server (point it at the server's base URL): +Official client packages. Each works against your self-hosted server (set `baseUrl`) or the [hosted API](https://openserp.org/cloud) (set `apiKey`): | Type | Package | Install | | --------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------- | @@ -80,6 +80,8 @@ Official client packages. Each works against your self-hosted server (point it a | MCP server (AI agents) | [`@openserp/mcp`](https://www.npmjs.com/package/@openserp/mcp) | `npx @openserp/mcp` | | n8n community node | [`@openserp/n8n-nodes-openserp`](https://www.npmjs.com/package/@openserp/n8n-nodes-openserp) | Install via n8n community nodes | +See [**examples**](./examples) for small JavaScript and Python use cases covering search, AI grounding, SEO, content extraction, and image search. + ```js import { OpenSERP } from "@openserp/sdk"; diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..8f94ee5 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,90 @@ +# OpenSERP Examples + +Short, copy-pasteable examples for using OpenSERP from JavaScript and Python. +Each one runs against a local server by default and works the same against the +[hosted API](#using-the-hosted-api) by swapping in an API key. + +## Start a server + +Most examples expect OpenSERP running on `http://localhost:7000`. + +```bash +# Docker +docker run -p 127.0.0.1:7000:7000 -it karust/openserp serve -a 0.0.0.0 -p 7000 + +# Or from source +go build -o openserp . && ./openserp serve +``` + +Prefer not to run a server? Skip ahead to [Using the hosted API](#using-the-hosted-api). + +## Try it without code + +A search is a single HTTP GET, so you can check the server with `curl`: + +```bash +# One engine +curl "http://localhost:7000/google/search?text=open+source+search+api&limit=10" + +# Several engines at once +curl "http://localhost:7000/mega/search?text=open+source+search+api&engines=bing,duckduckgo&limit=10" +``` + +## SDKs and integrations + +| Tool | Package | Install | +| --- | --- | --- | +| JavaScript / TypeScript | [`@openserp/sdk`](https://www.npmjs.com/package/@openserp/sdk) | `npm install @openserp/sdk` | +| Python | [`openserp`](https://pypi.org/project/openserp/) | `pip install openserp` | +| MCP server (AI agents) | [`@openserp/mcp`](https://www.npmjs.com/package/@openserp/mcp) | `npx @openserp/mcp` | +| n8n community node | [`@openserp/n8n-nodes-openserp`](https://www.npmjs.com/package/@openserp/n8n-nodes-openserp) | Install via n8n community nodes | + +## Examples by question + +### Getting started + +- **How do I run a search from code?** — [JavaScript](quickstart/js-basic-search) · [Python](quickstart/python-basic-search) +- **How do I compare results across several engines?** — [JavaScript](search/js-multi-engine-compare) +- **How do I search a list of keywords and export them?** — [Python](search/python-keyword-csv) + +### AI and LLM grounding + +- **How do I ground an LLM answer in fresh search results?** — [JavaScript](ai/js-rag-context-builder) +- **How do I give a search tool to an agent?** — [Python](ai/python-agent-tool) +- **I want this inside Claude, Cursor, or another MCP client.** — [`@openserp/mcp`](https://www.npmjs.com/package/@openserp/mcp) + +### SEO + +- **Who are my real competitors for a set of keywords?** — [JavaScript](seo/js-competitor-overlap) +- **Where does my domain rank for each keyword?** — [Python](seo/python-rank-tracker) + +### Content extraction + +- **How do I search and read the page content in one call?** — [JavaScript](content/js-search-with-extract) +- **How do I turn a URL into clean Markdown?** — [Python](content/python-extract-markdown) + +### Images + +- **How do I search images and preview them?** — [JavaScript](media/js-image-gallery) + +### Automation + +- **I want to wire OpenSERP into a no-code workflow.** — [`@openserp/n8n-nodes-openserp`](https://www.npmjs.com/package/@openserp/n8n-nodes-openserp) + +## Using the hosted API + +Every example points at a local server. To use the managed API instead, get a key +from [openserp.org/dashboard/keys](https://openserp.org/dashboard/keys) and construct +the client with it — no base URL needed: + +```js +const client = new OpenSERP({ apiKey: "" }); +``` + +```python +client = OpenSERP(api_key="") +``` + +The endpoints and response shape are identical, so example code moves between +self-hosted and hosted by changing only that one line. Set **either** `baseUrl` +(self-hosted) **or** `apiKey` (hosted) — not both. diff --git a/examples/ai/js-rag-context-builder/README.md b/examples/ai/js-rag-context-builder/README.md new file mode 100644 index 0000000..cba8741 --- /dev/null +++ b/examples/ai/js-rag-context-builder/README.md @@ -0,0 +1,11 @@ +# RAG context builder (JavaScript) + +Searches for a question and formats the top results into a ready-to-use prompt +with numbered, citable sources — the retrieval step of a RAG pipeline. + +```bash +npm install +node index.js +``` + +Edit the `question` in [index.js](index.js), then pass the printed prompt to your LLM. diff --git a/examples/ai/js-rag-context-builder/index.js b/examples/ai/js-rag-context-builder/index.js new file mode 100644 index 0000000..9bb5fc5 --- /dev/null +++ b/examples/ai/js-rag-context-builder/index.js @@ -0,0 +1,27 @@ +import { OpenSERP } from "@openserp/sdk"; + +// Hosted API instead? Get a key at https://openserp.org/dashboard/keys: +// const client = new OpenSERP({ apiKey: "" }); +const client = new OpenSERP({ baseUrl: "http://localhost:7000" }); + +const question = "What is retrieval augmented generation?"; + +const { results } = await client.search({ + engine: "google", + text: question, + limit: 10, +}); + +// Format the top results as numbered, citable context to drop into an LLM prompt. +const context = results + .map((r, i) => `[${i + 1}] ${r.title}\n${r.url}\n${r.snippet ?? ""}`) + .join("\n\n"); + +const prompt = `Answer the question using only the search results below. Cite sources as [n]. + +Question: ${question} + +Search results: +${context}`; + +console.log(prompt); diff --git a/examples/ai/js-rag-context-builder/package.json b/examples/ai/js-rag-context-builder/package.json new file mode 100644 index 0000000..307631f --- /dev/null +++ b/examples/ai/js-rag-context-builder/package.json @@ -0,0 +1,14 @@ +{ + "name": "openserp-example-js-rag-context-builder", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@openserp/sdk": "^0.2.0" + } +} diff --git a/examples/ai/python-agent-tool/README.md b/examples/ai/python-agent-tool/README.md new file mode 100644 index 0000000..af3e90e --- /dev/null +++ b/examples/ai/python-agent-tool/README.md @@ -0,0 +1,11 @@ +# Agent search tool (Python) + +A `web_search(query)` function shaped to register as a tool for an LLM agent. +It returns compact JSON-serializable results the model can read and cite. + +```bash +pip install -r requirements.txt +python main.py +``` + +Import `web_search` from [main.py](main.py) and register it with your agent framework. diff --git a/examples/ai/python-agent-tool/main.py b/examples/ai/python-agent-tool/main.py new file mode 100644 index 0000000..75747ab --- /dev/null +++ b/examples/ai/python-agent-tool/main.py @@ -0,0 +1,29 @@ +import json + +from openserp import OpenSERP + +# A search function shaped for use as an LLM / agent tool: it takes a query +# and returns plain JSON-serializable results the model can read. + + +def web_search(query: str, limit: int = 10) -> list[dict]: + """Search the web and return a compact list of results.""" + # Hosted API instead? Get a key at https://openserp.org/dashboard/keys: + # with OpenSERP(api_key="") as client: + with OpenSERP(base_url="http://localhost:7000") as client: + response = client.search(engine="google", text=query, limit=limit) + return [ + { + "rank": item.rank, + "title": item.title, + "url": item.url, + "snippet": item.snippet, + } + for item in response.results + ] + + +if __name__ == "__main__": + # Register web_search as a tool with your LLM; here we just call it directly. + results = web_search("what is retrieval augmented generation") + print(json.dumps(results, indent=2)) diff --git a/examples/ai/python-agent-tool/requirements.txt b/examples/ai/python-agent-tool/requirements.txt new file mode 100644 index 0000000..983ea85 --- /dev/null +++ b/examples/ai/python-agent-tool/requirements.txt @@ -0,0 +1 @@ +openserp>=0.2.0,<1 diff --git a/examples/content/js-search-with-extract/README.md b/examples/content/js-search-with-extract/README.md new file mode 100644 index 0000000..a9c8d43 --- /dev/null +++ b/examples/content/js-search-with-extract/README.md @@ -0,0 +1,11 @@ +# Search with extraction (JavaScript) + +Runs a search with `extract: true`, so OpenSERP fetches the top pages and returns +their cleaned content alongside each result — search and scrape in one request. + +```bash +npm install +node index.js +``` + +Edit the `text`, `extractTop`, or `extractMode` in [index.js](index.js) to tune it. diff --git a/examples/content/js-search-with-extract/index.js b/examples/content/js-search-with-extract/index.js new file mode 100644 index 0000000..2009ec9 --- /dev/null +++ b/examples/content/js-search-with-extract/index.js @@ -0,0 +1,29 @@ +import { OpenSERP } from "@openserp/sdk"; + +// Hosted API instead? Get a key at https://openserp.org/dashboard/keys: +// const client = new OpenSERP({ apiKey: "", timeoutMs: 60_000 }); +const client = new OpenSERP({ baseUrl: "http://localhost:7000", timeoutMs: 60_000 }); + +// `extract: true` fetches the top pages and returns their cleaned content +// alongside each result, so you get the page text in a single request. +// `extractTop` (max 5) controls how many results are enriched. +const { results } = await client.search({ + engine: "ecosia", + text: "what is a serp api", + extract: true, + extractTop: 2, + extractMode: "auto", +}); + +for (const item of results) { + console.log(`${item.rank}. ${item.title}`); + console.log(` ${item.url}`); + + // When extracted, `item.extracted.content` holds the page body. Trim it to + // a short preview here. + const content = item.extracted?.content; + if (content) { + console.log(` ${content.slice(0, 300).trim()}…`); + } + console.log(); +} diff --git a/examples/content/js-search-with-extract/package.json b/examples/content/js-search-with-extract/package.json new file mode 100644 index 0000000..2909552 --- /dev/null +++ b/examples/content/js-search-with-extract/package.json @@ -0,0 +1,14 @@ +{ + "name": "openserp-example-js-search-with-extract", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@openserp/sdk": "^0.2.0" + } +} diff --git a/examples/content/python-extract-markdown/README.md b/examples/content/python-extract-markdown/README.md new file mode 100644 index 0000000..3c26762 --- /dev/null +++ b/examples/content/python-extract-markdown/README.md @@ -0,0 +1,10 @@ +# Extract a page to Markdown (Python) + +Extracts one URL with `/extract` and writes the cleaned page content to `extracted.md`. + +```bash +pip install -r requirements.txt +python main.py +``` + +Edit the `url` in [main.py](main.py) to extract a different page. diff --git a/examples/content/python-extract-markdown/main.py b/examples/content/python-extract-markdown/main.py new file mode 100644 index 0000000..8064edc --- /dev/null +++ b/examples/content/python-extract-markdown/main.py @@ -0,0 +1,16 @@ +from openserp import OpenSERP + +url = "https://go.dev/doc/" +output_file = "extracted.md" + +# Hosted API instead? Get a key at https://openserp.org/dashboard/keys: +# with OpenSERP(api_key="", timeout=60.0) as client: +with OpenSERP(base_url="http://localhost:7000", timeout=60.0) as client: + # /extract turns a single page into clean Markdown (or plain text). + result = client.extract(url=url, mode="auto", clean=True) + +content = result.markdown or result.text or "" +with open(output_file, "w", encoding="utf-8") as handle: + handle.write(content) + +print(f"Extracted {len(content)} characters from {url} into {output_file}") diff --git a/examples/content/python-extract-markdown/requirements.txt b/examples/content/python-extract-markdown/requirements.txt new file mode 100644 index 0000000..983ea85 --- /dev/null +++ b/examples/content/python-extract-markdown/requirements.txt @@ -0,0 +1 @@ +openserp>=0.2.0,<1 diff --git a/examples/media/js-image-gallery/README.md b/examples/media/js-image-gallery/README.md new file mode 100644 index 0000000..876bf19 --- /dev/null +++ b/examples/media/js-image-gallery/README.md @@ -0,0 +1,10 @@ +# Image gallery (JavaScript) + +Runs an image search and writes a self-contained `gallery.html` you can open in a browser. + +```bash +npm install +node index.js +``` + +Edit the `query` in [index.js](index.js), then open `gallery.html`. diff --git a/examples/media/js-image-gallery/index.js b/examples/media/js-image-gallery/index.js new file mode 100644 index 0000000..d0fef26 --- /dev/null +++ b/examples/media/js-image-gallery/index.js @@ -0,0 +1,56 @@ +import { writeFile } from "node:fs/promises"; +import { OpenSERP } from "@openserp/sdk"; + +// Hosted API instead? Get a key at https://openserp.org/dashboard/keys: +// const client = new OpenSERP({ apiKey: "" }); +const client = new OpenSERP({ baseUrl: "http://localhost:7000" }); + +const query = "go gopher mascot"; + +const { results } = await client.image({ + engine: "bing", + text: query, + limit: 12, +}); + +const cards = results + .map((result) => { + const src = escapeHtml(result.image?.thumbnail ?? result.image?.url ?? ""); + const title = escapeHtml(result.title ?? "Untitled"); + const page = escapeHtml(result.source?.page_url ?? "#"); + return `${title}${title}`; + }) + .join("\n"); + +const html = ` + + + + + ${escapeHtml(query)} + + + +

${escapeHtml(query)}

+
${cards}
+ +`; + +await writeFile("gallery.html", html, "utf8"); +console.log(`Wrote ${results.length} images to gallery.html — open it in a browser.`); + +function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, (char) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + })[char]); +} diff --git a/examples/media/js-image-gallery/package.json b/examples/media/js-image-gallery/package.json new file mode 100644 index 0000000..75ae7ee --- /dev/null +++ b/examples/media/js-image-gallery/package.json @@ -0,0 +1,14 @@ +{ + "name": "openserp-example-js-image-gallery", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@openserp/sdk": "^0.2.0" + } +} diff --git a/examples/quickstart/js-basic-search/README.md b/examples/quickstart/js-basic-search/README.md new file mode 100644 index 0000000..199ee5e --- /dev/null +++ b/examples/quickstart/js-basic-search/README.md @@ -0,0 +1,11 @@ +# Basic search (JavaScript) + +Runs one Google search with `@openserp/sdk` and prints the titles and URLs. + +```bash +npm install +node index.js +``` + +Edit the `text`, `engine`, or `limit` in [index.js](index.js) to change the search. +To use the hosted API instead of a local server, follow the commented line at the top of the file. diff --git a/examples/quickstart/js-basic-search/index.js b/examples/quickstart/js-basic-search/index.js new file mode 100644 index 0000000..c07eeee --- /dev/null +++ b/examples/quickstart/js-basic-search/index.js @@ -0,0 +1,18 @@ +import { OpenSERP } from "@openserp/sdk"; + +// Hosted API instead? Get a key at https://openserp.org/dashboard/keys: +// const client = new OpenSERP({ apiKey: "" }); +const client = new OpenSERP({ baseUrl: "http://localhost:7000" }); + +const { results } = await client.search({ + engine: "google", + text: "open source search api", + region: "US", + lang: "EN", + limit: 10, +}); + +for (const item of results) { + console.log(`${item.rank}. ${item.title}`); + console.log(` ${item.url}`); +} diff --git a/examples/quickstart/js-basic-search/package.json b/examples/quickstart/js-basic-search/package.json new file mode 100644 index 0000000..6e1d12b --- /dev/null +++ b/examples/quickstart/js-basic-search/package.json @@ -0,0 +1,14 @@ +{ + "name": "openserp-example-js-basic-search", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@openserp/sdk": "^0.2.0" + } +} diff --git a/examples/quickstart/python-basic-search/README.md b/examples/quickstart/python-basic-search/README.md new file mode 100644 index 0000000..1643f7e --- /dev/null +++ b/examples/quickstart/python-basic-search/README.md @@ -0,0 +1,11 @@ +# Basic search (Python) + +Runs one Google search with the `openserp` SDK and prints the titles and URLs. + +```bash +pip install -r requirements.txt +python main.py +``` + +Edit the `text`, `engine`, or `limit` in [main.py](main.py) to change the search. +To use the hosted API instead of a local server, follow the commented line at the top of the file. diff --git a/examples/quickstart/python-basic-search/main.py b/examples/quickstart/python-basic-search/main.py new file mode 100644 index 0000000..8c32e27 --- /dev/null +++ b/examples/quickstart/python-basic-search/main.py @@ -0,0 +1,16 @@ +from openserp import OpenSERP + +# Hosted API instead? Get a key at https://openserp.org/dashboard/keys: +# with OpenSERP(api_key="") as client: +with OpenSERP(base_url="http://localhost:7000") as client: + response = client.search( + engine="google", + text="open source search api", + region="US", + lang="EN", + limit=10, + ) + + for item in response.results: + print(f"{item.rank}. {item.title}") + print(f" {item.url}") diff --git a/examples/quickstart/python-basic-search/requirements.txt b/examples/quickstart/python-basic-search/requirements.txt new file mode 100644 index 0000000..983ea85 --- /dev/null +++ b/examples/quickstart/python-basic-search/requirements.txt @@ -0,0 +1 @@ +openserp>=0.2.0,<1 diff --git a/examples/search/js-multi-engine-compare/README.md b/examples/search/js-multi-engine-compare/README.md new file mode 100644 index 0000000..f1e12e2 --- /dev/null +++ b/examples/search/js-multi-engine-compare/README.md @@ -0,0 +1,11 @@ +# Multi-engine compare (JavaScript) + +Runs one query across several engines with `/mega/search` and groups the results +by domain, so you can see which sites the engines agree on. + +```bash +npm install +node index.js +``` + +Edit the `query` and `engines` in [index.js](index.js) to compare your own. diff --git a/examples/search/js-multi-engine-compare/index.js b/examples/search/js-multi-engine-compare/index.js new file mode 100644 index 0000000..9240e6e --- /dev/null +++ b/examples/search/js-multi-engine-compare/index.js @@ -0,0 +1,34 @@ +import { OpenSERP } from "@openserp/sdk"; + +// Hosted API instead? Get a key at https://openserp.org/dashboard/keys: +// const client = new OpenSERP({ apiKey: "" }); +const client = new OpenSERP({ baseUrl: "http://localhost:7000" }); + +const query = "privacy focused search engine"; + +// /mega/search runs one query across several engines and merges the results. +const { results } = await client.megaSearch({ + text: query, + engines: ["bing", "duckduckgo"], + region: "US", + limit: 10, +}); + +// Group results by domain to see which sites both engines agree on. +const byDomain = new Map(); +for (const result of results) { + const domain = result.domain; + if (!domain) continue; + + const stats = byDomain.get(domain) ?? { hits: 0, engines: new Set(), bestRank: Infinity }; + stats.hits += 1; + stats.engines.add(result.engine); + stats.bestRank = Math.min(stats.bestRank, result.rank ?? Infinity); + byDomain.set(domain, stats); +} + +console.log(`Domains found for "${query}":\n`); +const ranked = [...byDomain].sort((a, b) => a[1].bestRank - b[1].bestRank); +for (const [domain, stats] of ranked) { + console.log(`${domain} (rank ${stats.bestRank}, seen in ${[...stats.engines].join(", ")})`); +} diff --git a/examples/search/js-multi-engine-compare/package.json b/examples/search/js-multi-engine-compare/package.json new file mode 100644 index 0000000..892cc1a --- /dev/null +++ b/examples/search/js-multi-engine-compare/package.json @@ -0,0 +1,14 @@ +{ + "name": "openserp-example-js-multi-engine-compare", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@openserp/sdk": "^0.2.0" + } +} diff --git a/examples/search/python-keyword-csv/README.md b/examples/search/python-keyword-csv/README.md new file mode 100644 index 0000000..eb7b379 --- /dev/null +++ b/examples/search/python-keyword-csv/README.md @@ -0,0 +1,10 @@ +# Keyword list to CSV (Python) + +Searches a list of keywords and writes every result to `keywords.csv`. + +```bash +pip install -r requirements.txt +python main.py +``` + +Edit the `keywords` list in [main.py](main.py) to export your own. diff --git a/examples/search/python-keyword-csv/main.py b/examples/search/python-keyword-csv/main.py new file mode 100644 index 0000000..f8bbeee --- /dev/null +++ b/examples/search/python-keyword-csv/main.py @@ -0,0 +1,31 @@ +import csv + +from openserp import OpenSERP + + +keywords = ["search api", "open source serp"] +output_file = "keywords.csv" + +# Hosted API instead? Get a key at https://openserp.org/dashboard/keys: +#with OpenSERP(api_key="") as client: +with OpenSERP(base_url="http://localhost:7000") as client: + rows = [] + for keyword in keywords: + response = client.search(engine="google", text=keyword, region="US", limit=10) + for result in response.results: + rows.append( + { + "keyword": keyword, + "rank": result.rank, + "title": result.title, + "url": result.url, + "domain": result.domain, + } + ) + +with open(output_file, "w", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter(handle, fieldnames=["keyword", "rank", "title", "url", "domain"]) + writer.writeheader() + writer.writerows(rows) + +print(f"Wrote {len(rows)} rows to {output_file}") diff --git a/examples/search/python-keyword-csv/requirements.txt b/examples/search/python-keyword-csv/requirements.txt new file mode 100644 index 0000000..983ea85 --- /dev/null +++ b/examples/search/python-keyword-csv/requirements.txt @@ -0,0 +1 @@ +openserp>=0.2.0,<1 diff --git a/examples/seo/js-competitor-overlap/README.md b/examples/seo/js-competitor-overlap/README.md new file mode 100644 index 0000000..5a4b2fa --- /dev/null +++ b/examples/seo/js-competitor-overlap/README.md @@ -0,0 +1,11 @@ +# Competitor overlap (JavaScript) + +Searches several keywords and ranks the domains by how many of them they appear in — +the sites showing up across your keyword set are your real SERP competitors. + +```bash +npm install +node index.js +``` + +Edit the `keywords` list in [index.js](index.js) to match your niche. diff --git a/examples/seo/js-competitor-overlap/index.js b/examples/seo/js-competitor-overlap/index.js new file mode 100644 index 0000000..341beda --- /dev/null +++ b/examples/seo/js-competitor-overlap/index.js @@ -0,0 +1,32 @@ +import { OpenSERP } from "@openserp/sdk"; + +// Hosted API instead? Get a key at https://openserp.org/dashboard/keys: +// const client = new OpenSERP({ apiKey: "" }); +const client = new OpenSERP({ baseUrl: "http://localhost:7000" }); + +// Domains that rank for several of your keywords are your real SERP competitors. +const keywords = ["serp api", "google search api", "scrape search results"]; + +const byDomain = new Map(); +for (const keyword of keywords) { + const { results } = await client.search({ + engine: "google", + text: keyword, + region: "US", + limit: 10, + }); + + for (const result of results) { + if (!result.domain) continue; + const stats = byDomain.get(result.domain) ?? { keywords: new Set(), bestRank: Infinity }; + stats.keywords.add(keyword); + stats.bestRank = Math.min(stats.bestRank, result.rank ?? Infinity); + byDomain.set(result.domain, stats); + } +} + +console.log(`Competitor overlap across ${keywords.length} keywords:\n`); +const ranked = [...byDomain].sort((a, b) => b[1].keywords.size - a[1].keywords.size); +for (const [domain, stats] of ranked) { + console.log(`${domain} — ${stats.keywords.size}/${keywords.length} keywords, best rank ${stats.bestRank}`); +} diff --git a/examples/seo/js-competitor-overlap/package.json b/examples/seo/js-competitor-overlap/package.json new file mode 100644 index 0000000..4d5c9a6 --- /dev/null +++ b/examples/seo/js-competitor-overlap/package.json @@ -0,0 +1,14 @@ +{ + "name": "openserp-example-js-competitor-overlap", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@openserp/sdk": "^0.2.0" + } +} diff --git a/examples/seo/python-rank-tracker/README.md b/examples/seo/python-rank-tracker/README.md new file mode 100644 index 0000000..f55be1d --- /dev/null +++ b/examples/seo/python-rank-tracker/README.md @@ -0,0 +1,11 @@ +# Rank tracker (Python) + +Checks where a target domain ranks for a list of keywords and writes the results +to `rankings.csv`. Handles `www.` and subdomains when matching the domain. + +```bash +pip install -r requirements.txt +python main.py +``` + +Edit `target_domain` and `keywords` in [main.py](main.py) to track your own site. diff --git a/examples/seo/python-rank-tracker/main.py b/examples/seo/python-rank-tracker/main.py new file mode 100644 index 0000000..baf9e88 --- /dev/null +++ b/examples/seo/python-rank-tracker/main.py @@ -0,0 +1,38 @@ +import csv + +from openserp import OpenSERP + +target_domain = "go.dev" +keywords = ["golang tutorial", "go programming language", "golang documentation"] +output_file = "rankings.csv" + + +def matches(domain: str | None, target: str) -> bool: + if not domain: + return False + domain = domain.lower().removeprefix("www.") + return domain == target or domain.endswith(f".{target}") + + +# Hosted API instead? Get a key at https://openserp.org/dashboard/keys: +# with OpenSERP(api_key="") as client: +with OpenSERP(base_url="http://localhost:7000") as client: + rows = [] + for keyword in keywords: + response = client.search(engine="google", text=keyword, region="US", limit=20) + hit = next((r for r in response.results if matches(r.domain, target_domain)), None) + rows.append( + { + "keyword": keyword, + "rank": hit.rank if hit else "not in top 20", + "url": hit.url if hit else "", + } + ) + print(f'"{keyword}": {rows[-1]["rank"]}') + +with open(output_file, "w", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter(handle, fieldnames=["keyword", "rank", "url"]) + writer.writeheader() + writer.writerows(rows) + +print(f"\nSaved rankings for {target_domain} to {output_file}") diff --git a/examples/seo/python-rank-tracker/requirements.txt b/examples/seo/python-rank-tracker/requirements.txt new file mode 100644 index 0000000..983ea85 --- /dev/null +++ b/examples/seo/python-rank-tracker/requirements.txt @@ -0,0 +1 @@ +openserp>=0.2.0,<1