seo_geo 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/AGENTS.md +244 -0
- data/CHANGELOG.md +40 -0
- data/LICENSE.txt +21 -0
- data/README.md +428 -0
- data/app/controllers/seo_geo/index_now_controller.rb +13 -0
- data/app/controllers/seo_geo/llms_controller.rb +29 -0
- data/app/controllers/seo_geo/og_images_controller.rb +67 -0
- data/app/controllers/seo_geo/robots_controller.rb +14 -0
- data/app/controllers/seo_geo/sitemaps_controller.rb +16 -0
- data/app/jobs/seo_geo/index_now_job.rb +21 -0
- data/app/views/seo_geo/fallback/_body.html.erb +21 -0
- data/app/views/seo_geo/fallback/_faq.html.erb +12 -0
- data/config/routes.rb +17 -0
- data/lib/generators/seo_geo/install/install_generator.rb +138 -0
- data/lib/generators/seo_geo/install/templates/home.yml.tt +14 -0
- data/lib/generators/seo_geo/install/templates/initializer.rb.tt +52 -0
- data/lib/seo_geo/audit/checks.rb +107 -0
- data/lib/seo_geo/audit/html_document.rb +132 -0
- data/lib/seo_geo/audit/report.rb +85 -0
- data/lib/seo_geo/audit/runner.rb +327 -0
- data/lib/seo_geo/audit/static_scan.rb +60 -0
- data/lib/seo_geo/audit.rb +14 -0
- data/lib/seo_geo/bot_verifier.rb +278 -0
- data/lib/seo_geo/configuration.rb +832 -0
- data/lib/seo_geo/controller.rb +72 -0
- data/lib/seo_geo/crawler_fallback.rb +45 -0
- data/lib/seo_geo/crawlers.rb +79 -0
- data/lib/seo_geo/engine.rb +123 -0
- data/lib/seo_geo/index_now.rb +187 -0
- data/lib/seo_geo/json_ld.rb +259 -0
- data/lib/seo_geo/llms_txt.rb +183 -0
- data/lib/seo_geo/markdown/document.rb +163 -0
- data/lib/seo_geo/markdown/negotiation.rb +49 -0
- data/lib/seo_geo/markdown/path_suffix.rb +72 -0
- data/lib/seo_geo/markdownable.rb +141 -0
- data/lib/seo_geo/meta.rb +382 -0
- data/lib/seo_geo/model.rb +95 -0
- data/lib/seo_geo/og_image/card.html.erb +22 -0
- data/lib/seo_geo/og_image/ferrum_backend.rb +72 -0
- data/lib/seo_geo/og_image/renderer.rb +163 -0
- data/lib/seo_geo/page.rb +201 -0
- data/lib/seo_geo/path_match.rb +36 -0
- data/lib/seo_geo/publication.rb +68 -0
- data/lib/seo_geo/robots/evaluator.rb +85 -0
- data/lib/seo_geo/robots.rb +105 -0
- data/lib/seo_geo/robots_directives.rb +170 -0
- data/lib/seo_geo/robots_path.rb +125 -0
- data/lib/seo_geo/rspec.rb +197 -0
- data/lib/seo_geo/safe_regexp.rb +50 -0
- data/lib/seo_geo/sitemap/registry.rb +239 -0
- data/lib/seo_geo/sitemap/xml.rb +138 -0
- data/lib/seo_geo/testing/environment.rb +31 -0
- data/lib/seo_geo/testing.rb +111 -0
- data/lib/seo_geo/undeclared_host.rb +40 -0
- data/lib/seo_geo/version.rb +5 -0
- data/lib/seo_geo.rb +80 -0
- data/lib/tasks/seo_geo.rake +23 -0
- data/llms.txt +27 -0
- metadata +224 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ec8b0f1b5e2f745e00c47a2d2f9a15e2eda6ca9d0357948feca1a22548c6ec25
|
|
4
|
+
data.tar.gz: 8eb32a70fd40779db958d83a6927229c7b09ba156e630472cb4325759cdbec91
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 01636e3e7a699db78db0765973b16652cd6130c2ed1aea1c463632c3b8b55cd1576bdbedfcbf20c0f0f95139ee64c9579a735d3129bc7af1815960a94881389d
|
|
7
|
+
data.tar.gz: 33f4593adfa66ddebdf586bdc21473749eb60df802d242da5c2b8be2fc983a48af5338da4ab55f65b46272768070e02cafeb870cf3c896888fd30120d4ac7c63
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# AGENTS.md — seo_geo (Ruby / Rails)
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding agents that add this gem to an application or work on this repository. Humans: the
|
|
4
|
+
[README](README.md) has the same facts with more explanation.
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
A Rails engine for SEO and GEO. GEO is Generative Engine Optimization: visibility in AI search and assistants
|
|
9
|
+
(ChatGPT, Claude, Perplexity, Gemini). It is **not** a geolocation, GeoIP or maps gem. One initializer and one
|
|
10
|
+
layout line produce the head (title, description, canonical, hreflang, Open Graph, JSON-LD), per-host
|
|
11
|
+
`robots.txt`, `sitemap.xml`, `llms.txt`/`llms-full.txt`, Markdown responses for agents, IndexNow pings, OG
|
|
12
|
+
images and an audit task.
|
|
13
|
+
|
|
14
|
+
## When to use it
|
|
15
|
+
|
|
16
|
+
**Use it when** a Rails 7.2–8.1 app (ERB, Hotwire or Inertia) needs search and AI-crawler visibility managed
|
|
17
|
+
in one place: several hostnames folded onto one canonical origin, a crawler policy by purpose (training vs
|
|
18
|
+
search vs user-triggered fetch), FAQ copy that must match its JSON-LD, or Inertia/SPA pages that crawlers see as
|
|
19
|
+
empty.
|
|
20
|
+
|
|
21
|
+
**Do not use it when**
|
|
22
|
+
|
|
23
|
+
- you need geolocation (IP to country, maps) — this gem does not do that;
|
|
24
|
+
- the app is not Rails (plain Rack, Sinatra, Hanami) — the engine needs Railties and ActionPack;
|
|
25
|
+
- you want to promise rich results or rankings — the gem emits valid markup, nothing more;
|
|
26
|
+
- a CDN or framework already owns `/robots.txt`, `/sitemap.xml` and the `<head>`; run one source, not two.
|
|
27
|
+
|
|
28
|
+
## Integration recipe
|
|
29
|
+
|
|
30
|
+
1. `bundle add seo_geo`, then `bin/rails g seo_geo:install https://www.example.com`. The generator writes
|
|
31
|
+
`config/initializers/seo_geo.rb` and `config/seo_geo/pages/home.yml`. It mounts the engine as the last
|
|
32
|
+
route, before a top-level catch-all if there is one.
|
|
33
|
+
2. Delete `public/robots.txt`, `public/sitemap.xml` and `public/llms.txt` if they exist: static files win over
|
|
34
|
+
the engine routes.
|
|
35
|
+
3. Configure hosts, crawler purposes, the sitemap and llms.txt:
|
|
36
|
+
|
|
37
|
+
<!-- example: quickstart/config/initializers/seo_geo.rb -->
|
|
38
|
+
```ruby
|
|
39
|
+
# config/initializers/seo_geo.rb
|
|
40
|
+
SeoGeo.configure do |seo|
|
|
41
|
+
seo.site.name = "Example"
|
|
42
|
+
seo.site.description = "Example helps small teams plan their week."
|
|
43
|
+
seo.canonical_origin = "https://www.example.com"
|
|
44
|
+
seo.canonical_aliases = ["example.com"] # folded onto www.example.com
|
|
45
|
+
seo.host "www.example.com", disallow: ["/admin"] # every crawler group repeats this
|
|
46
|
+
seo.crawlers.training = :deny # GPTBot, ClaudeBot, CCBot, ... => Disallow: /
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
SeoGeo.sitemap.draw do
|
|
50
|
+
add "/", priority: 1.0
|
|
51
|
+
add "/pricing"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
SeoGeo.config.llms_txt.page :home, path: "/"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
4. Replace the app's `<title>`, meta description, canonical link and JSON-LD with one line. `seo_geo_head`
|
|
58
|
+
emits exactly one `<title>`; remove any other.
|
|
59
|
+
|
|
60
|
+
<!-- example: quickstart/app/views/layouts/application.html.erb -->
|
|
61
|
+
```erb
|
|
62
|
+
<%# app/views/layouts/application.html.erb %>
|
|
63
|
+
<!DOCTYPE html>
|
|
64
|
+
<html lang="<%= seo_geo_html_lang %>">
|
|
65
|
+
<head>
|
|
66
|
+
<meta charset="utf-8">
|
|
67
|
+
<%= seo_geo_head %>
|
|
68
|
+
</head>
|
|
69
|
+
<body>
|
|
70
|
+
<main><%= yield %></main>
|
|
71
|
+
</body>
|
|
72
|
+
</html>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
5. Set per-page values in controllers: `seo title:, description:` for one-off pages, `seo_geo_page(:key)` for
|
|
76
|
+
pages with registered copy. Include `SeoGeo::Markdownable` where agents should get Markdown.
|
|
77
|
+
|
|
78
|
+
<!-- example: quickstart/app/controllers/pages_controller.rb -->
|
|
79
|
+
```ruby
|
|
80
|
+
# app/controllers/pages_controller.rb
|
|
81
|
+
class PagesController < ApplicationController
|
|
82
|
+
include SeoGeo::Markdownable # Accept: text/markdown and /pricing.md
|
|
83
|
+
|
|
84
|
+
def home
|
|
85
|
+
@page = seo_geo_page(:home) # title, description and FAQPage JSON-LD from home.yml
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def pricing
|
|
89
|
+
seo title: "Pricing", description: "Free for up to three people; paid plans for larger teams, billed monthly."
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
6. **Inertia** (React/Vue/Svelte): pass `seo_geo_page(:key).props` as a prop and render the visible copy from
|
|
95
|
+
it. In the layout, render `seo_geo_fallback_body(page)` after `yield`. After the client app mounts, call
|
|
96
|
+
`window.SeoGeo?.mounted()`. Use `seo_geo_markdown page: :key` so Markdown comes from the copy rather than the
|
|
97
|
+
empty `#app` div:
|
|
98
|
+
|
|
99
|
+
<!-- example: inertia/app/controllers/pages_controller.rb -->
|
|
100
|
+
```ruby
|
|
101
|
+
# app/controllers/pages_controller.rb (inertia_rails)
|
|
102
|
+
class PagesController < ApplicationController
|
|
103
|
+
include SeoGeo::Markdownable
|
|
104
|
+
seo_geo_markdown only: :home, page: :home # Markdown from the page copy, not the empty #app div
|
|
105
|
+
|
|
106
|
+
def home
|
|
107
|
+
render inertia: "Home", props: { seo: seo_geo_page(:home).props }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
<!-- example: inertia/app/views/layouts/application.html.erb -->
|
|
113
|
+
```erb
|
|
114
|
+
<%# app/views/layouts/application.html.erb (Inertia) %>
|
|
115
|
+
<!DOCTYPE html>
|
|
116
|
+
<html lang="<%= seo_geo_html_lang %>">
|
|
117
|
+
<head>
|
|
118
|
+
<meta charset="utf-8">
|
|
119
|
+
<%= seo_geo_head %>
|
|
120
|
+
</head>
|
|
121
|
+
<body>
|
|
122
|
+
<%= yield %>
|
|
123
|
+
<% if (page = seo_geo_state.page) %>
|
|
124
|
+
<%= seo_geo_fallback_body(page) %>
|
|
125
|
+
<% end %>
|
|
126
|
+
</body>
|
|
127
|
+
</html>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
7. Add integration tests with the gem's assertions. They run the audit's own checks:
|
|
131
|
+
|
|
132
|
+
<!-- example: quickstart/test/integration/seo_test.rb -->
|
|
133
|
+
```ruby
|
|
134
|
+
# test/integration/seo_test.rb
|
|
135
|
+
require "test_helper"
|
|
136
|
+
require "seo_geo/testing"
|
|
137
|
+
|
|
138
|
+
class SeoTest < ActionDispatch::IntegrationTest
|
|
139
|
+
include SeoGeo::Testing::Assertions
|
|
140
|
+
|
|
141
|
+
test "one title and a folded canonical" do
|
|
142
|
+
assert_seo_geo_single_title "/"
|
|
143
|
+
assert_seo_geo_canonical "/pricing", host: "example.com", expected: "https://www.example.com/pricing"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
test "FAQ JSON-LD matches the visible text" do
|
|
147
|
+
assert_seo_geo_faq_matches_visible "/"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
test "production robots.txt and sitemap" do
|
|
151
|
+
with_seo_geo_environment("production") do
|
|
152
|
+
refute_seo_geo_robots_allows "GPTBot", "/", host: "www.example.com"
|
|
153
|
+
assert_seo_geo_robots_allows "OAI-SearchBot", "/pricing", host: "www.example.com"
|
|
154
|
+
refute_seo_geo_robots_allows "OAI-SearchBot", "/admin", host: "www.example.com"
|
|
155
|
+
assert_seo_geo_sitemap host: "www.example.com"
|
|
156
|
+
assert_seo_geo_no_sitemap host: "example.com"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
RSpec: `require "seo_geo/rspec"` gives `have_seo_geo_canonical`, `have_seo_geo_single_title`,
|
|
163
|
+
`have_seo_geo_faq_matching_visible_text`, `be_seo_geo_valid_sitemap(origin:)` and `seo_geo_robots_allow(ua, path)`.
|
|
164
|
+
|
|
165
|
+
## Invariants — do not violate
|
|
166
|
+
|
|
167
|
+
1. **Hosts fail closed.** Every hostname that serves the app is either `canonical_origin`'s host, listed in
|
|
168
|
+
`canonical_aliases`/`canonical_exclude_hosts`, or declared with `seo.host`. Undeclared hosts (staging,
|
|
169
|
+
`*.onrender.com`) get `Disallow: /` and no sitemap. Never "fix" a closed host by allowing everything.
|
|
170
|
+
2. **One copy source per page.** Text that appears in FAQPage JSON-LD comes from `SeoGeo.page(:key)` and is
|
|
171
|
+
rendered from the same object (`seo_geo_faq`, `seo_geo_fallback_body`, `Page#props`). Never hand-write a
|
|
172
|
+
second copy in a view or component, and never add JSON-LD for content that is not visible.
|
|
173
|
+
3. **The User-Agent is not identity.** Do not branch access, content or rate limits on `request.user_agent`
|
|
174
|
+
(`rake seo_geo:audit` warns: `code.user_agent_branch`). To trust a bot, add
|
|
175
|
+
`config.middleware.use SeoGeo::BotVerifier::Middleware` and require `request.env["seo_geo.bot"][:verified] == true`.
|
|
176
|
+
4. **Never invent `lastmod`.** No `Time.now`, `Date.today` or `updated_at` of an unrelated record. Use a real
|
|
177
|
+
column (`lastmod: :updated_at`) or omit it. No made-up prices, ratings or counts in JSON-LD either.
|
|
178
|
+
5. **Noindex in one place.** A page that must stay out of search goes in `noindex_paths` or
|
|
179
|
+
`sitemap.add path, noindex: true`, which keeps it out of sitemap.xml and llms.txt and renders robots
|
|
180
|
+
noindex. A request-time `seo noindex: true` alone cannot reach sitemap.xml.
|
|
181
|
+
6. **Mount last.** `mount SeoGeo::Engine, at: "/"` stays below the app's routes and above any catch-all.
|
|
182
|
+
7. **`:full_access` is opt-in.** `crawlers.override "Mediapartners-Google", :full_access` ignores every host
|
|
183
|
+
`disallow` for that token. Never use it for AI crawlers or as a way to open a path; use `allow:` instead.
|
|
184
|
+
8. **Collections are lambdas.** `add -> { Post.published }, path: ...`. A scope passed directly loads the model
|
|
185
|
+
at boot and keeps a stale class across development reloads. `select` must include `id` (records are read
|
|
186
|
+
with `find_each`).
|
|
187
|
+
|
|
188
|
+
## Check commands
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
bundle exec rake test # this repository: unit, docs examples, both dummy apps
|
|
192
|
+
bin/rails seo_geo:audit # your app: exits 1 on errors; FORMAT=json HOSTS=a,b MAX_PAGES=200
|
|
193
|
+
SEO_GEO_AUDIT=1 bin/rails runner 'puts SeoGeo::Audit::Runner.new(app: Rails.application, root: Rails.root).run'
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The rake task audits as production even in development: it sets `SEO_GEO_AUDIT=1` itself, so the engine adds
|
|
197
|
+
the configured hosts to `config.hosts` for that run (`seo_geo audit: temporarily allowing ... in config.hosts`).
|
|
198
|
+
A runner needs the variable before boot. Production `config.hosts` is never widened.
|
|
199
|
+
|
|
200
|
+
With the app running (`bin/rails s`), check what crawlers and agents receive. robots.txt and sitemap.xml only
|
|
201
|
+
open up in `indexable_environments` (default production):
|
|
202
|
+
|
|
203
|
+
<!-- curl: quickstart -->
|
|
204
|
+
```sh
|
|
205
|
+
curl -s -H "Host: www.example.com" http://localhost:3000/robots.txt
|
|
206
|
+
curl -s -H "Host: unknown.example.net" http://localhost:3000/robots.txt # Disallow: /
|
|
207
|
+
curl -s -H "Host: www.example.com" http://localhost:3000/sitemap.xml
|
|
208
|
+
curl -s -H "Host: example.com" http://localhost:3000/sitemap.xml # => 404 (alias)
|
|
209
|
+
curl -s -H "Host: www.example.com" http://localhost:3000/llms.txt
|
|
210
|
+
curl -si -H "Host: www.example.com" -H "Accept: text/markdown" http://localhost:3000/pricing
|
|
211
|
+
curl -si -H "Host: www.example.com" http://localhost:3000/index.md
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Troubleshooting
|
|
215
|
+
|
|
216
|
+
See the README's [Troubleshooting](README.md#troubleshooting) table (exact error messages, causes and fixes).
|
|
217
|
+
|
|
218
|
+
## Working on this repository
|
|
219
|
+
|
|
220
|
+
- Verify with `bundle exec rake test`: unit tests, docs examples, then app tests against `test/dummy` (ERB)
|
|
221
|
+
and `test/dummy_inertia`, each dummy in its own process. Other Rails versions:
|
|
222
|
+
`BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test` (also `rails_8_0`, `rails_8_1`).
|
|
223
|
+
- [docs/DESIGN.md](docs/DESIGN.md) is the contract. Names and signatures in § Public API and § Shared contract
|
|
224
|
+
are fixed; changing one means updating DESIGN.md in the same change. New configuration keys get a default, a
|
|
225
|
+
YARD comment, an `errors` entry when they can be invalid, and a test in `test/unit/configuration_test.rb`.
|
|
226
|
+
- Shared files have append-only zones: `lib/seo_geo.rb` (require list), `lib/seo_geo/engine.rb` (one named
|
|
227
|
+
initializer per topic), `config/routes.rb` and `CHANGELOG.md` (one bullet per change under Unreleased).
|
|
228
|
+
- Reuse the shared primitives instead of re-implementing them: `SeoGeo::RobotsPath` (RFC 9309 path
|
|
229
|
+
normalization), `SeoGeo::RobotsDirectives`, `SeoGeo::SafeRegexp`, `SeoGeo::Publication.publishable?`, and
|
|
230
|
+
`config.canonical_url` / `config.host_policy`.
|
|
231
|
+
- Tests: `test/unit/**` boots no Rails app; `test/app/**` runs against both dummies (`inertia_dummy?` to branch);
|
|
232
|
+
`test/docs/**` boots `docs/examples/` apps in subprocesses and checks README/AGENTS code blocks against them.
|
|
233
|
+
A README or AGENTS code block in Ruby, ERB, YAML or JS needs a `<!-- example: path -->` marker and a file
|
|
234
|
+
under `docs/examples/`; output blocks use `<!-- output: ... -->` and are compared with real responses.
|
|
235
|
+
- Every Ruby file under `lib/` and `app/` starts with `# frozen_string_literal: true`; public methods carry YARD
|
|
236
|
+
comments.
|
|
237
|
+
|
|
238
|
+
## Related packages
|
|
239
|
+
|
|
240
|
+
- [meta-tags](https://github.com/kpumuk/meta-tags): the tag renderer seo_geo builds on; `set_meta_tags` values
|
|
241
|
+
still apply.
|
|
242
|
+
- [inertia_rails](https://github.com/inertiajs/inertia-rails): pair with `Page#props` and `seo_geo_fallback_body`.
|
|
243
|
+
- [webmcp](https://github.com/seunghan91/webmcp): lets in-browser AI agents call your app's endpoints as tools.
|
|
244
|
+
It complements seo_geo, which is about being read rather than being operated.
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.1.0 - 2026-10-14
|
|
6
|
+
|
|
7
|
+
First release.
|
|
8
|
+
|
|
9
|
+
- Configuration: one `SeoGeo.configure` (also `config.seo_geo`) validated at boot — production server boots raise,
|
|
10
|
+
production generator/rake tasks and non-production boots warn; purpose-based crawler table
|
|
11
|
+
(`crawlers.training/search/user`, per-token `override` including opt-in `:full_access`); per-host policies with
|
|
12
|
+
fail-closed unknown hosts (logged once per host in indexable environments); alias hosts inherit the canonical
|
|
13
|
+
host's policy; canonical URL folding (`canonical_aliases`, `canonical_exclude_hosts`, `canonical_keep_params`).
|
|
14
|
+
- Head: `seo_geo_head` (single `<title>` via `title_template`, description truncation, folded canonical, hreflang +
|
|
15
|
+
x-default, robots noindex for non-indexable hosts/environments, `default_noindex` and declared noindex paths,
|
|
16
|
+
verification metas, Open Graph/Twitter with `og:image` size only when known, `og:locale` from one locale rule,
|
|
17
|
+
Markdown alternate link, JSON-LD `@graph` with CSP nonce and `json_escape`); controller `seo(...)` and
|
|
18
|
+
`seo_geo_page`; `SeoGeo::JsonLd` builder with required-field checks; crawler fallback body (`seo_geo_fallback_body`
|
|
19
|
+
with an extra slot, `seo_geo_faq`) and `window.SeoGeo.mounted()`.
|
|
20
|
+
- Page copy: `SeoGeo.page(key)` from Ruby, I18n or YAML — one source for crawler HTML, FAQPage JSON-LD and
|
|
21
|
+
Inertia props (`Page#props`, `intro` as String or Array).
|
|
22
|
+
- Crawler files: per-host `GET /robots.txt` (RFC 9309; every named group carries the host rules; Content-Signal,
|
|
23
|
+
optional AIPREF Content-Usage, Daum PIN), `GET /sitemap.xml` with a sitemap index above 50,000 URLs and lazy
|
|
24
|
+
collection sources (`add -> { scope }`, `batch: false`), `GET /llms.txt` and `/llms-full.txt`.
|
|
25
|
+
- One publication rule (`SeoGeo::Publication`) behind sitemap.xml, llms.txt and the head: robots disallow,
|
|
26
|
+
exclusions, `noindex_paths` / `sitemap.add ..., noindex: true`, `default_noindex` + `index: true` (with `match:`
|
|
27
|
+
for collections). Paths compare in RFC 9309 form (`SeoGeo::RobotsPath`); configuration Regexps run with a timeout
|
|
28
|
+
(`SeoGeo::SafeRegexp`).
|
|
29
|
+
- Markdown for agents: `SeoGeo::Markdownable` (Accept negotiation and `.md` URLs — `markdown.path_suffix` turns the
|
|
30
|
+
URL form off — YAML frontmatter, JSON-LD fences, `Vary: Accept`, `Content-Signal`, robots directives carried into
|
|
31
|
+
`X-Robots-Tag`), page-copy Markdown for Inertia mount points.
|
|
32
|
+
- IndexNow: `SeoGeo::IndexNow.submit`, `SeoGeo::IndexNowJob`, dry run outside indexable environments,
|
|
33
|
+
`/<key>.txt`, model macro `seo_geo_indexnow`.
|
|
34
|
+
- OG images: `GET /og/:key.png` with a pluggable backend or the optional ferrum renderer, routed only when
|
|
35
|
+
configured and only for known keys, constrained params, cache TTL, concurrency cap and fallback redirect.
|
|
36
|
+
- Audit and testing: `rake seo_geo:audit` (in-process, works with a development `config.hosts`), `rails g
|
|
37
|
+
seo_geo:install` (mounts the engine after the app's routes), Minitest assertions and RSpec matchers
|
|
38
|
+
(`with_seo_geo_environment`), `SeoGeo::Robots::Evaluator`, opt-in `SeoGeo::BotVerifier`.
|
|
39
|
+
- CI: Ruby 3.2–3.4 × Rails 7.2/8.0/8.1, each against an ERB app and an Inertia-style app; README and AGENTS.md
|
|
40
|
+
examples run in the test suite.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 seunghan Kim
|
|
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.
|