Skip to main content

indexnow_api/
lib.rs

1//! # indexnow-api
2//!
3//! Async [IndexNow](https://www.indexnow.org/) client for Rust.
4//!
5//! IndexNow is an open protocol that lets a website tell search engines the
6//! moment a URL is added, updated or deleted, instead of waiting for the next
7//! crawl. One request reaches every participating engine: Microsoft Bing,
8//! Yandex, Naver, Seznam.cz and Yep. (Google does not support IndexNow.)
9//!
10//! This crate wraps the `POST /IndexNow` JSON endpoint with a small
11//! [`reqwest`]-based client. There is no other runtime dependency; bring your
12//! own async runtime such as Tokio.
13//!
14//! ## Quick start
15//!
16//! ```no_run
17//! use indexnow_api::IndexNowApi;
18//!
19//! #[tokio::main]
20//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
21//!     // `host` is your site's hostname, `key` is your IndexNow key
22//!     // (the same string you host at https://<host>/<key>.txt).
23//!     let api = IndexNowApi::new("www.example.com", "7be9fca90b3b4b039983fa8f06e03ee8");
24//!
25//!     // Up to 10,000 URLs per request. Anything that implements `ToString` works.
26//!     api.send_urls(vec![
27//!         "https://www.example.com/new-page",
28//!         "https://www.example.com/updated-article",
29//!     ])
30//!     .await?;
31//!     Ok(())
32//! }
33//! ```
34//!
35//! ## Choosing a search engine endpoint
36//!
37//! By default requests go to `https://api.indexnow.org`, which forwards the
38//! submission to all IndexNow engines. Use [`IndexNowApi::set_search_engine`]
39//! to target one engine directly; pass the origin only, the `/IndexNow` path
40//! is appended by the crate.
41//!
42//! ```no_run
43//! use indexnow_api::IndexNowApi;
44//!
45//! # async fn run() -> Result<(), indexnow_api::IndexNowError> {
46//! let mut api = IndexNowApi::new("www.example.com", "7be9fca90b3b4b039983fa8f06e03ee8");
47//! api.set_search_engine("https://www.bing.com");
48//! api.set_key_location("https://www.example.com/keys/7be9fca90b3b4b039983fa8f06e03ee8.txt");
49//! api.send_urls(vec!["https://www.example.com/"]).await?;
50//! # Ok(())
51//! # }
52//! ```
53//!
54//! ## Errors
55//!
56//! [`send_urls`](IndexNowApi::send_urls) returns [`IndexNowError`] when the
57//! request cannot be sent ([`IndexNowError::Connection`]) or the endpoint
58//! answers with a status other than 200 or 202 ([`IndexNowError::Status`],
59//! carrying the code and the response body). The error implements
60//! [`std::error::Error`] and [`std::fmt::Display`].
61
62mod error;
63mod http;
64
65use crate::http::HttpClient;
66pub use error::*;
67use serde::{Deserialize, Serialize};
68
69/// IndexNow client bound to one website (`host`) and one IndexNow key.
70///
71/// Create it with [`IndexNowApi::new`], optionally change the endpoint with
72/// [`set_search_engine`](IndexNowApi::set_search_engine) or the key file
73/// location with [`set_key_location`](IndexNowApi::set_key_location), then
74/// call [`send_urls`](IndexNowApi::send_urls).
75pub struct IndexNowApi {
76    search_engine: String,
77    host: String,
78    key: String,
79    key_location: Option<String>,
80}
81
82impl IndexNowApi {
83    /// Creates a client for `host` authenticated with `key`.
84    ///
85    /// * `host` – hostname of the site whose URLs you will submit, e.g. `www.example.com`.
86    /// * `key` – your IndexNow key: 8 to 128 characters of `a-z`, `A-Z`, `0-9` and `-`.
87    ///   The same key must be served as plain text at `https://<host>/<key>.txt`
88    ///   (or at the URL given to [`set_key_location`](IndexNowApi::set_key_location)).
89    ///
90    /// The endpoint defaults to `https://api.indexnow.org`.
91    pub fn new<T: ToString, U: ToString>(host: T, key: U) -> IndexNowApi {
92        IndexNowApi {
93            search_engine: "https://api.indexnow.org".to_string(),
94            host: host.to_string(),
95            key: key.to_string(),
96            key_location: None,
97        }
98    }
99
100    /// Sets the search engine origin to submit to. Pass the origin without the
101    /// `/IndexNow` path, for example `https://www.bing.com` or `https://yandex.com`.
102    ///
103    /// Default: `https://api.indexnow.org` (shared endpoint that notifies every
104    /// participating engine).
105    pub fn set_search_engine<T: ToString>(&mut self, search_engine: T) {
106        self.search_engine = search_engine.to_string()
107    }
108
109    /// Sets the `keyLocation` field: the full URL of your key file when it is
110    /// not at the default `https://<host>/<key>.txt`.
111    pub fn set_key_location<T: ToString>(&mut self, key_location: T) {
112        self.key_location = Some(key_location.to_string())
113    }
114
115    /// Submits `urls` to the search engine as one `POST /IndexNow` request.
116    ///
117    /// All URLs must belong to `host`. IndexNow accepts up to 10,000 URLs per
118    /// request. Returns `Ok(())` when the endpoint answers with HTTP 200 or
119    /// 202 (accepted, key validation pending), [`IndexNowError::Status`] for
120    /// any other status and [`IndexNowError::Connection`] when the request
121    /// could not be sent.
122    pub async fn send_urls<T: ToString>(&self, urls: Vec<T>) -> Result<(), IndexNowError> {
123        HttpClient::post(
124            format!("{}/IndexNow", self.search_engine).as_str(),
125            SendData {
126                url_list: urls.iter().map(|q| q.to_string()).collect(),
127                host: self.host.to_string(),
128                key: self.key.to_string(),
129                key_location: self.key_location.clone(),
130            },
131        )
132        .await
133    }
134}
135
136/// JSON body of an IndexNow submission
137/// (`{"host": ..., "key": ..., "keyLocation": ..., "urlList": [...]}`).
138///
139/// Built internally by [`IndexNowApi::send_urls`]; exposed for inspection and
140/// serialization only.
141#[derive(Default, Debug, Serialize, Deserialize, Clone)]
142pub struct SendData {
143    #[serde(rename = "urlList")]
144    url_list: Vec<String>,
145    host: String,
146    key: String,
147    #[serde(rename = "keyLocation")]
148    key_location: Option<String>,
149}