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}