index_now 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +101 -0
- data/app/models/index_now/announcement.rb +23 -0
- data/db/migrate/20261028000000_create_index_now_announcements.rb +10 -0
- data/lib/generators/index_now/install/install_generator.rb +35 -0
- data/lib/generators/index_now/install/templates/create_index_now_announcements.rb.tt +10 -0
- data/lib/generators/index_now/install/templates/initializer.rb.tt +5 -0
- data/lib/index_now/client.rb +56 -0
- data/lib/index_now/configuration.rb +17 -0
- data/lib/index_now/engine.rb +9 -0
- data/lib/index_now/result.rb +9 -0
- data/lib/index_now/sitemap.rb +55 -0
- data/lib/index_now/version.rb +3 -0
- data/lib/index_now.rb +55 -0
- data/lib/tasks/index_now.rake +30 -0
- metadata +87 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 131220889a57d1c72c46f1332899034547b103c7b9594d06de1b1f06dd3937d4
|
|
4
|
+
data.tar.gz: a78d03b12c476c130d364ad1d2a91ff9cfcbe3b1928994570730157d7db6dffc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 36d2dce172e838142c225200bb726dfdd72da7b0a14e11f53576583474ebf0dbe2d627c009f11dd39178ed424792cfa91c723908b193d61fd7faf2c240714b5c
|
|
7
|
+
data.tar.gz: 96f998799a9fdf85f2af8f5aa5228f084e8d299708892756b2b4f24e7eda6e4ca55749fca322e0e70c133f79f71c6c3b34d5baede34ee30b54367f2ecec9b196
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] — 2026-10-28
|
|
4
|
+
|
|
5
|
+
- First release: announces sitemap URLs whose `lastmod` moved to IndexNow,
|
|
6
|
+
records what was sent, and never raises from `IndexNow.announce_changes`.
|
|
7
|
+
- `index_now:announce`, `index_now:pending` and `index_now:announce_all` tasks.
|
|
8
|
+
- `index_now:install` generator for the key file, initializer and migration.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anton Kopylov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# index_now
|
|
2
|
+
|
|
3
|
+
Tells search engines which pages of a Rails app changed, through
|
|
4
|
+
[IndexNow](https://www.indexnow.org), so they recrawl them without waiting.
|
|
5
|
+
|
|
6
|
+
It reads the app's own sitemap, announces every URL whose `<lastmod>` moved
|
|
7
|
+
since it last announced that URL, and records what it sent. Running it when
|
|
8
|
+
nothing changed sends nothing, so it is safe to call as often as you like.
|
|
9
|
+
|
|
10
|
+
**What it reaches:** `api.indexnow.org` shares each submission with Bing (which
|
|
11
|
+
also backs ChatGPT search and Copilot), Yandex, Seznam, Naver and Yep.
|
|
12
|
+
**Google is not an IndexNow participant**; nothing here affects Google.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem "index_now"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
bin/rails generate index_now:install # or --key=<existing key>
|
|
22
|
+
bin/rails db:migrate
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The generator writes:
|
|
26
|
+
|
|
27
|
+
- `public/<key>.txt`: the key file the engines fetch to verify you own the host.
|
|
28
|
+
The key is not a secret; commit it.
|
|
29
|
+
- `config/initializers/index_now.rb`
|
|
30
|
+
- a migration for `index_now_announcements`, one row per URL announced.
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
IndexNow.configure do |config|
|
|
34
|
+
config.key = "…"
|
|
35
|
+
config.host = "example.com" # defaults to routes.default_url_options[:host]
|
|
36
|
+
config.sitemap_path = "/sitemap.xml" # the default
|
|
37
|
+
config.enabled = Rails.env.production? # the default
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The sitemap is requested in-process (`Rails.application.call`) as
|
|
42
|
+
`https://<host><sitemap_path>`, so the host must pass `config.hosts`.
|
|
43
|
+
|
|
44
|
+
## The rule
|
|
45
|
+
|
|
46
|
+
A URL is announced when it has never been announced, or when its `<lastmod>`
|
|
47
|
+
is newer than the one last announced for it. A URL with no `<lastmod>` is
|
|
48
|
+
announced once and never again, so give every page you want re-announced a
|
|
49
|
+
`<lastmod>`:
|
|
50
|
+
|
|
51
|
+
- **Copy:** a date you bump by hand when the page's text changes. Never the
|
|
52
|
+
deploy time; that announces every page on every deploy.
|
|
53
|
+
- **Data:** the time the data behind the page last changed.
|
|
54
|
+
|
|
55
|
+
`<sitemapindex>` files are not supported.
|
|
56
|
+
|
|
57
|
+
## Triggering it
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
IndexNow.announce_changes # => #<data IndexNow::Result urls=[…], error=nil>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Call it after the data is committed: the sitemap is read on a separate thread
|
|
64
|
+
and connection. It never raises: a failure is logged, reported through `Rails.error`, and
|
|
65
|
+
returned as `result.error`. Nothing is recorded for a URL until the POST that
|
|
66
|
+
carried it succeeded, so the next call retries it.
|
|
67
|
+
|
|
68
|
+
**After a deploy** (for copy), from `.kamal/hooks/post-deploy`:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
if [ "$KAMAL_DESTINATION" = "production" ]; then
|
|
72
|
+
bin/kamal app exec -d production --reuse --primary "bin/rails index_now:announce" || true
|
|
73
|
+
fi
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**After the app changes data** (for data pages), from the job that wrote it:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
def perform
|
|
80
|
+
Stats.rebuild!
|
|
81
|
+
IndexNow.announce_changes
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Tasks
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
bin/rails index_now:pending # what the next announce would send
|
|
89
|
+
bin/rails index_now:announce # send it; exits 1 on failure, says so when disabled
|
|
90
|
+
bin/rails index_now:announce_all # send every sitemap URL again
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Rotating the key
|
|
94
|
+
|
|
95
|
+
Rename `public/<old key>.txt` to the new key, put the same key in its body and in
|
|
96
|
+
`config.key`, deploy, check `https://<host>/<key>.txt` answers 200, then run
|
|
97
|
+
`index_now:announce_all`.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module IndexNow
|
|
2
|
+
class Announcement < ActiveRecord::Base
|
|
3
|
+
self.table_name = "index_now_announcements"
|
|
4
|
+
|
|
5
|
+
def self.due(entries)
|
|
6
|
+
announced = where(url: entries.map(&:url)).pluck(:url, :lastmod).to_h
|
|
7
|
+
|
|
8
|
+
entries.select do |entry|
|
|
9
|
+
next true unless announced.key?(entry.url)
|
|
10
|
+
|
|
11
|
+
previous = announced[entry.url]
|
|
12
|
+
entry.lastmod && (previous.nil? || entry.lastmod > previous)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.record!(entries)
|
|
17
|
+
return if entries.empty?
|
|
18
|
+
|
|
19
|
+
now = Time.current
|
|
20
|
+
upsert_all(entries.map { |entry| { url: entry.url, lastmod: entry.lastmod, announced_at: now } }, unique_by: :url)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class CreateIndexNowAnnouncements < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :index_now_announcements do |t|
|
|
4
|
+
t.string :url, null: false
|
|
5
|
+
t.datetime :lastmod
|
|
6
|
+
t.datetime :announced_at, null: false
|
|
7
|
+
end
|
|
8
|
+
add_index :index_now_announcements, :url, unique: true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module IndexNow
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
include Rails::Generators::Migration
|
|
9
|
+
|
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
|
11
|
+
class_option :key, type: :string, desc: "Reuse an existing IndexNow key instead of generating one"
|
|
12
|
+
|
|
13
|
+
def self.next_migration_number(dirname) = ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
14
|
+
|
|
15
|
+
def create_key_file
|
|
16
|
+
create_file "public/#{key}.txt", key
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_initializer
|
|
20
|
+
template "initializer.rb.tt", "config/initializers/index_now.rb"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def copy_migration
|
|
24
|
+
migration_template "create_index_now_announcements.rb.tt", "db/migrate/create_index_now_announcements.rb"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
def key
|
|
29
|
+
@key ||= (options[:key] || SecureRandom.hex(16)).tap do |key|
|
|
30
|
+
raise ArgumentError, "an IndexNow key is 8 to 128 letters, digits or dashes" unless key.match?(/\A[a-zA-Z0-9-]{8,128}\z/)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class CreateIndexNowAnnouncements < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :index_now_announcements do |t|
|
|
4
|
+
t.string :url, null: false
|
|
5
|
+
t.datetime :lastmod
|
|
6
|
+
t.datetime :announced_at, null: false
|
|
7
|
+
end
|
|
8
|
+
add_index :index_now_announcements, :url, unique: true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module IndexNow
|
|
5
|
+
class SubmitFailed < Error; end
|
|
6
|
+
|
|
7
|
+
module Client
|
|
8
|
+
ENDPOINT = URI("https://api.indexnow.org/indexnow").freeze
|
|
9
|
+
BATCH = 10_000
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def submit!(urls)
|
|
13
|
+
return 0 if urls.empty?
|
|
14
|
+
|
|
15
|
+
host = host_of(urls)
|
|
16
|
+
urls.each_slice(BATCH) { |batch| post(host, batch) }
|
|
17
|
+
urls.size
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
def host_of(urls)
|
|
22
|
+
host = URI(urls.first).host
|
|
23
|
+
foreign = urls.reject { |url| URI(url).host == host }
|
|
24
|
+
raise SubmitFailed, "not on #{host}, refusing to submit: #{foreign.join(", ")}" if foreign.any?
|
|
25
|
+
|
|
26
|
+
host
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def post(host, urls)
|
|
30
|
+
request = Net::HTTP::Post.new(ENDPOINT)
|
|
31
|
+
request["Content-Type"] = "application/json; charset=utf-8"
|
|
32
|
+
request.body = JSON.generate(host: host, key: key, keyLocation: key_location(host), urlList: urls)
|
|
33
|
+
|
|
34
|
+
response = Net::HTTP.start(ENDPOINT.host, ENDPOINT.port, use_ssl: true, open_timeout: 10, read_timeout: 20) do |http|
|
|
35
|
+
http.request(request)
|
|
36
|
+
end
|
|
37
|
+
code = response.code.to_i
|
|
38
|
+
raise SubmitFailed, failure_message(code, host) unless [ 200, 202 ].include?(code)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def key = IndexNow.config.key
|
|
42
|
+
|
|
43
|
+
def key_location(host) = "https://#{host}/#{key}.txt"
|
|
44
|
+
|
|
45
|
+
def failure_message(code, host)
|
|
46
|
+
case code
|
|
47
|
+
when 400 then "400 malformed request: the JSON body was rejected"
|
|
48
|
+
when 403 then "403 key not valid: #{key_location(host)} must be reachable and contain exactly the key"
|
|
49
|
+
when 422 then "422 the urls do not belong to #{host}, or the key does not match the schema"
|
|
50
|
+
when 429 then "429 too many requests: rate limited as potential spam"
|
|
51
|
+
else "IndexNow answered HTTP #{code}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module IndexNow
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :key, :sitemap_path
|
|
4
|
+
attr_writer :host, :enabled
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@sitemap_path = "/sitemap.xml"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def host
|
|
11
|
+
@host || Rails.application.routes.default_url_options[:host] ||
|
|
12
|
+
raise(Error, "IndexNow has no host: set config.host or routes.default_url_options[:host]")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def enabled = @enabled.nil? ? Rails.env.production? : @enabled
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "nokogiri"
|
|
2
|
+
|
|
3
|
+
module IndexNow
|
|
4
|
+
class SitemapError < Error; end
|
|
5
|
+
|
|
6
|
+
module Sitemap
|
|
7
|
+
Entry = Data.define(:url, :lastmod)
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def entries
|
|
11
|
+
parse(fetch).xpath("/urlset/url").map do |url|
|
|
12
|
+
Entry.new(url: url.at("loc").text.strip, lastmod: url.at("lastmod")&.text&.then { |text| lastmod(text) })
|
|
13
|
+
end.uniq(&:url)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
def url = "https://#{IndexNow.config.host}#{IndexNow.config.sitemap_path}"
|
|
18
|
+
|
|
19
|
+
# Own thread: Rails.application.call runs the executor, which would reset the caller's Current and query cache.
|
|
20
|
+
def fetch = Thread.new { request }.value
|
|
21
|
+
|
|
22
|
+
def request
|
|
23
|
+
status, _headers, body = Rails.application.call(Rack::MockRequest.env_for(url, "HTTP_HOST" => IndexNow.config.host))
|
|
24
|
+
content = +""
|
|
25
|
+
body.each { |chunk| content << chunk }
|
|
26
|
+
raise SitemapError, failure_message(status) unless status == 200
|
|
27
|
+
|
|
28
|
+
content
|
|
29
|
+
ensure
|
|
30
|
+
body.close if body.respond_to?(:close)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def failure_message(status)
|
|
34
|
+
hint = " (is #{IndexNow.config.host} in config.hosts?)" if status == 403
|
|
35
|
+
"#{status} from #{url}#{hint}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse(xml)
|
|
39
|
+
document = Nokogiri::XML(xml, &:strict).remove_namespaces!
|
|
40
|
+
root = document.root&.name
|
|
41
|
+
raise SitemapError, "#{url} is a #{root || "empty document"}, not a urlset" unless root == "urlset"
|
|
42
|
+
|
|
43
|
+
document
|
|
44
|
+
rescue Nokogiri::XML::SyntaxError => error
|
|
45
|
+
raise SitemapError, "#{url} is not an XML urlset: #{error.message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def lastmod(text)
|
|
49
|
+
text = text.strip
|
|
50
|
+
text = "#{text}T00:00:00Z" if text.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
51
|
+
Time.iso8601(text.sub(/T(\d{2}:\d{2})(?=Z|[+-])/, 'T\1:00')).utc.floor(6)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/index_now.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
require "index_now/version"
|
|
3
|
+
|
|
4
|
+
module IndexNow
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require "index_now/configuration"
|
|
9
|
+
require "index_now/engine"
|
|
10
|
+
require "index_now/result"
|
|
11
|
+
require "index_now/sitemap"
|
|
12
|
+
require "index_now/client"
|
|
13
|
+
|
|
14
|
+
module IndexNow
|
|
15
|
+
class << self
|
|
16
|
+
def config = @config ||= Configuration.new
|
|
17
|
+
|
|
18
|
+
def configure = yield(config)
|
|
19
|
+
|
|
20
|
+
def reset_config! = @config = nil
|
|
21
|
+
|
|
22
|
+
def pending = Announcement.due(Sitemap.entries)
|
|
23
|
+
|
|
24
|
+
def announce_changes = announce { pending }
|
|
25
|
+
|
|
26
|
+
def announce_all = announce { Sitemap.entries }
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
# Never raises: a failed announcement must not fail the deploy or job that asked for it.
|
|
30
|
+
def announce(&entries)
|
|
31
|
+
return dry_run(&entries) unless config.enabled
|
|
32
|
+
|
|
33
|
+
entries = entries.call
|
|
34
|
+
urls = entries.map(&:url)
|
|
35
|
+
Client.submit!(urls)
|
|
36
|
+
Announcement.record!(entries)
|
|
37
|
+
Rails.logger.info("IndexNow announced #{urls.join(", ")}") if urls.any?
|
|
38
|
+
Result.new(urls: urls)
|
|
39
|
+
rescue StandardError => error
|
|
40
|
+
Rails.logger.error("IndexNow failed: #{error.class}: #{error.message}")
|
|
41
|
+
Rails.error.report(error, handled: true, source: "index_now")
|
|
42
|
+
Result.new(error: error)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Staging and development often cannot read their own sitemap (basic auth, config.hosts); that is not an error there.
|
|
46
|
+
def dry_run
|
|
47
|
+
urls = yield.map(&:url)
|
|
48
|
+
Rails.logger.info("IndexNow disabled; would announce #{urls.join(", ")}") if urls.any?
|
|
49
|
+
Result.new(disabled: true)
|
|
50
|
+
rescue StandardError => error
|
|
51
|
+
Rails.logger.info("IndexNow disabled; could not read the sitemap: #{error.message}")
|
|
52
|
+
Result.new(disabled: true)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
namespace :index_now do
|
|
2
|
+
report = lambda do |result|
|
|
3
|
+
abort "IndexNow failed: #{result.error.message}" unless result.ok?
|
|
4
|
+
|
|
5
|
+
if result.disabled?
|
|
6
|
+
puts "IndexNow is disabled in #{Rails.env}; nothing sent"
|
|
7
|
+
elsif result.urls.empty?
|
|
8
|
+
puts "nothing to announce"
|
|
9
|
+
else
|
|
10
|
+
puts "announced #{result.urls.size} url#{"s" unless result.urls.one?}"
|
|
11
|
+
result.urls.each { |url| puts " #{url}" }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "Announce sitemap urls whose lastmod moved since they were last announced"
|
|
16
|
+
task announce: :environment do
|
|
17
|
+
report.call(IndexNow.announce_changes)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "Announce every sitemap url again, e.g. after rotating the key"
|
|
21
|
+
task announce_all: :environment do
|
|
22
|
+
report.call(IndexNow.announce_all)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "List the urls the next announce would send"
|
|
26
|
+
task pending: :environment do
|
|
27
|
+
urls = IndexNow.pending.map(&:url)
|
|
28
|
+
puts urls.empty? ? "nothing to announce" : urls
|
|
29
|
+
end
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: index_now
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Anton Kopylov
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: nokogiri
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.16'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.16'
|
|
40
|
+
description: Reads the app's own sitemap, announces every URL whose lastmod moved
|
|
41
|
+
since it was last announced to api.indexnow.org, and records what it sent so nothing
|
|
42
|
+
is announced twice.
|
|
43
|
+
executables: []
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- CHANGELOG.md
|
|
48
|
+
- LICENSE.txt
|
|
49
|
+
- README.md
|
|
50
|
+
- app/models/index_now/announcement.rb
|
|
51
|
+
- db/migrate/20261028000000_create_index_now_announcements.rb
|
|
52
|
+
- lib/generators/index_now/install/install_generator.rb
|
|
53
|
+
- lib/generators/index_now/install/templates/create_index_now_announcements.rb.tt
|
|
54
|
+
- lib/generators/index_now/install/templates/initializer.rb.tt
|
|
55
|
+
- lib/index_now.rb
|
|
56
|
+
- lib/index_now/client.rb
|
|
57
|
+
- lib/index_now/configuration.rb
|
|
58
|
+
- lib/index_now/engine.rb
|
|
59
|
+
- lib/index_now/result.rb
|
|
60
|
+
- lib/index_now/sitemap.rb
|
|
61
|
+
- lib/index_now/version.rb
|
|
62
|
+
- lib/tasks/index_now.rake
|
|
63
|
+
homepage: https://github.com/tonic20/index_now
|
|
64
|
+
licenses:
|
|
65
|
+
- MIT
|
|
66
|
+
metadata:
|
|
67
|
+
homepage_uri: https://github.com/tonic20/index_now
|
|
68
|
+
source_code_uri: https://github.com/tonic20/index_now
|
|
69
|
+
changelog_uri: https://github.com/tonic20/index_now/blob/main/CHANGELOG.md
|
|
70
|
+
rdoc_options: []
|
|
71
|
+
require_paths:
|
|
72
|
+
- lib
|
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '3.3'
|
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
requirements: []
|
|
84
|
+
rubygems_version: 3.7.2
|
|
85
|
+
specification_version: 4
|
|
86
|
+
summary: Announce a Rails app's changed sitemap URLs to IndexNow
|
|
87
|
+
test_files: []
|