helios-sitemap 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 36a3e496ed0ede36fbe60b6fe276b5fd11f45b2c3a161bb101aac6202d0e46a8
4
+ data.tar.gz: 9213f58ce9365b6bc844c2ea4ed3fc62ca12ac60c8cd3647e341b0b0c01102f4
5
+ SHA512:
6
+ metadata.gz: 5f001b4b6a6fd4fa495452272da7ed859eb19ad9cfd2409466640bc033b2b276256ad259c34df4a44314155b4bd214baa76f29b83981f5613e79ed22f2c24d95
7
+ data.tar.gz: 49c10dea795a509dd3fb2e3904db5c75d30584a43533054b70693d5a19b1df02dcac0c01a3cc69193377736db52e04376af9546375c2e377740d5098269f4b67
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright TODO: Write your name
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Helios::Sitemap
2
+
3
+ Sitemap generation with S3 storage and IndexNow submission for Rails. Designed for ephemeral disk systems (Heroku, Docker) where you can't persist generated sitemaps to disk.
4
+
5
+ **Flow:** Generate sitemap -> Upload to S3 -> Serve from your app at `/sitemap.xml` -> Submit to IndexNow
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem "helios-sitemap"
13
+ ```
14
+
15
+ Then `bundle install`.
16
+
17
+ ## Configuration
18
+
19
+ Create an initializer at `config/initializers/helios_sitemap.rb`:
20
+
21
+ ```ruby
22
+ Helios::Sitemap.configure do |config|
23
+ config.default_host = "https://example.com"
24
+
25
+ # S3 storage (defaults to ENV vars if not set)
26
+ config.aws_bucket = ENV["AWS_SITEMAP_BUCKET"]
27
+ config.aws_region = ENV["AWS_REGION"]
28
+ config.aws_access_key_id = ENV["AWS_ACCESS_KEY_ID"]
29
+ config.aws_secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
30
+
31
+ # IndexNow (defaults to ENV vars if not set)
32
+ config.indexnow_domain = ENV["INDEXNOW_DOMAIN"]
33
+ config.indexnow_api_key = ENV["INDEXNOW_API_KEY"]
34
+
35
+ # Define what goes in your sitemap
36
+ config.sitemap_entries = ->(sitemap) {
37
+ sitemap.add "/", changefreq: "weekly", priority: 1.0
38
+ Post.published.find_each do |post|
39
+ sitemap.add "/#{post.slug}", changefreq: "weekly", priority: 0.7
40
+ end
41
+ }
42
+
43
+ # Define URLs to submit to IndexNow
44
+ config.indexnow_urls = -> {
45
+ urls = ["https://example.com/"]
46
+ Post.published.find_each do |post|
47
+ urls << "https://example.com/#{post.slug}"
48
+ end
49
+ urls
50
+ }
51
+ end
52
+ ```
53
+
54
+ ## Routes
55
+
56
+ Mount the engine in your `config/routes.rb`:
57
+
58
+ ```ruby
59
+ mount Helios::Sitemap::Engine, at: "/"
60
+ ```
61
+
62
+ This provides:
63
+ - `GET /sitemap.xml` - Serves the sitemap XML from S3
64
+ - `GET /sitemap.xml.gz` - Serves the gzipped sitemap from S3
65
+
66
+ ## Generating & Uploading
67
+
68
+ Trigger a sitemap refresh by running the job:
69
+
70
+ ```ruby
71
+ Helios::Sitemap::RefreshJob.perform_later
72
+ ```
73
+
74
+ You can schedule this however you prefer (cron, recurring job, after publishing content, etc.).
75
+
76
+ ## Environment Variables
77
+
78
+ | Variable | Description |
79
+ |---|---|
80
+ | `AWS_SITEMAP_BUCKET` | S3 bucket name |
81
+ | `AWS_REGION` | AWS region |
82
+ | `AWS_ACCESS_KEY_ID` | AWS access key |
83
+ | `AWS_SECRET_ACCESS_KEY` | AWS secret key |
84
+ | `INDEXNOW_DOMAIN` | Your domain for IndexNow |
85
+ | `INDEXNOW_API_KEY` | Your IndexNow API key |
86
+
87
+ ## License
88
+
89
+ Proprietary. All rights reserved.
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,6 @@
1
+ module Helios
2
+ module Sitemap
3
+ class ApplicationController < ActionController::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aws-sdk-s3"
4
+ require "zlib"
5
+
6
+ module Helios
7
+ module Sitemap
8
+ class SitemapController < ::ActionController::Base
9
+ def show
10
+ config = Helios::Sitemap.configuration
11
+
12
+ gz_data = config.s3_client.get_object(
13
+ bucket: config.aws_bucket,
14
+ key: config.s3_object_key
15
+ ).body.read
16
+
17
+ if request.path.end_with?(".gz")
18
+ render plain: gz_data,
19
+ content_type: "application/gzip",
20
+ content_disposition: 'attachment; filename="sitemap.xml.gz"'
21
+ else
22
+ xml_data = Zlib::GzipReader.new(StringIO.new(gz_data)).read
23
+ render plain: xml_data, content_type: "application/xml"
24
+ end
25
+ rescue Aws::S3::Errors::NoSuchKey
26
+ head :not_found
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,6 @@
1
+ module Helios
2
+ module Sitemap
3
+ module ApplicationHelper
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Helios
2
+ module Sitemap
3
+ class ApplicationJob < ActiveJob::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sitemap_generator"
4
+ require "aws-sdk-s3"
5
+
6
+ module Helios
7
+ module Sitemap
8
+ class RefreshJob < ApplicationJob
9
+ queue_as :default
10
+
11
+ def perform
12
+ config = Helios::Sitemap.configuration
13
+
14
+ urls = collect_urls(config)
15
+
16
+ generate_sitemap(config)
17
+ upload_to_s3(config) unless Rails.env.development?
18
+ submit_to_indexnow(urls)
19
+ end
20
+
21
+ private
22
+
23
+ def generate_sitemap(config)
24
+ Rails.logger.info("[helios-sitemap] Generating sitemap...")
25
+
26
+ SitemapGenerator::Sitemap.default_host = config.default_host
27
+
28
+ entries_proc = config.sitemap_entries
29
+ SitemapGenerator::Sitemap.create do
30
+ entries_proc&.call(self)
31
+ end
32
+ end
33
+
34
+ def upload_to_s3(config)
35
+ file_path = Rails.root.join("public", "sitemap.xml.gz")
36
+
37
+ transfer_manager = Aws::S3::TransferManager.new(client: config.s3_client)
38
+
39
+ transfer_manager.upload_file(
40
+ file_path,
41
+ bucket: config.aws_bucket,
42
+ key: config.s3_object_key,
43
+ content_type: "application/gzip"
44
+ )
45
+
46
+ Rails.logger.info("[helios-sitemap] Uploaded sitemap to s3://#{config.aws_bucket}/#{config.s3_object_key}")
47
+ end
48
+
49
+ def collect_urls(config)
50
+ return [] unless config.indexnow_urls
51
+
52
+ config.indexnow_urls.call
53
+ end
54
+
55
+ def submit_to_indexnow(urls)
56
+ return unless urls.any?
57
+
58
+ Rails.logger.info("[helios-sitemap] Submitting #{urls.count} URLs to IndexNow")
59
+ IndexNowService.submit_urls(urls)
60
+ rescue IndexNowService::IndexNowError => e
61
+ Rails.logger.error("[helios-sitemap] IndexNow configuration error: #{e.message}")
62
+ rescue StandardError => e
63
+ Rails.logger.error("[helios-sitemap] Unexpected error submitting to IndexNow: #{e.class} - #{e.message}")
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,8 @@
1
+ module Helios
2
+ module Sitemap
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: "from@example.com"
5
+ layout "mailer"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module Helios
2
+ module Sitemap
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Helios sitemap</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= yield :head %>
9
+
10
+ <%= stylesheet_link_tag "helios/sitemap/application", media: "all" %>
11
+ </head>
12
+ <body>
13
+
14
+ <%= yield %>
15
+
16
+ </body>
17
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ Helios::Sitemap::Engine.routes.draw do
2
+ get "sitemap.xml", to: "sitemap#show"
3
+ get "sitemap.xml.gz", to: "sitemap#show"
4
+ end
@@ -0,0 +1,33 @@
1
+ module Helios
2
+ module Sitemap
3
+ class Configuration
4
+ attr_accessor :default_host,
5
+ :aws_bucket,
6
+ :aws_region,
7
+ :aws_access_key_id,
8
+ :aws_secret_access_key,
9
+ :s3_object_key,
10
+ :indexnow_domain,
11
+ :indexnow_api_key,
12
+ :sitemap_entries,
13
+ :indexnow_urls
14
+
15
+ def initialize
16
+ @s3_object_key = "sitemaps/sitemap.xml.gz"
17
+ @aws_region = ENV["AWS_REGION"]
18
+ @aws_bucket = ENV["AWS_SITEMAP_BUCKET"]
19
+ @aws_access_key_id = ENV["AWS_ACCESS_KEY_ID"]
20
+ @aws_secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
21
+ @indexnow_domain = ENV["INDEXNOW_DOMAIN"]
22
+ @indexnow_api_key = ENV["INDEXNOW_API_KEY"]
23
+ end
24
+
25
+ def s3_client
26
+ Aws::S3::Client.new(
27
+ region: aws_region,
28
+ credentials: Aws::Credentials.new(aws_access_key_id, aws_secret_access_key)
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ module Helios
2
+ module Sitemap
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace Helios::Sitemap
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module Helios
7
+ module Sitemap
8
+ class IndexNowService
9
+ ENDPOINT = "https://api.indexnow.org/IndexNow"
10
+
11
+ class IndexNowError < StandardError; end
12
+
13
+ class << self
14
+ def submit_urls(urls)
15
+ raise IndexNowError, "URLs array cannot be empty" if urls.blank?
16
+
17
+ payload = build_payload(urls)
18
+ make_request(payload)
19
+ rescue IndexNowError
20
+ raise
21
+ rescue StandardError => e
22
+ Rails.logger.error "IndexNow: Unexpected error - #{e.class}: #{e.message}"
23
+ false
24
+ end
25
+
26
+ private
27
+
28
+ def build_payload(urls)
29
+ {
30
+ host: domain,
31
+ key: api_key,
32
+ keyLocation: key_location_url,
33
+ urlList: urls
34
+ }
35
+ end
36
+
37
+ def make_request(payload)
38
+ uri = URI(ENDPOINT)
39
+ http = Net::HTTP.new(uri.host, uri.port)
40
+ http.use_ssl = true
41
+ http.read_timeout = 10
42
+ http.open_timeout = 10
43
+
44
+ request = Net::HTTP::Post.new(uri.path)
45
+ request["Content-Type"] = "application/json; charset=utf-8"
46
+ request.body = payload.to_json
47
+
48
+ response = http.request(request)
49
+ handle_response(response)
50
+ end
51
+
52
+ def handle_response(response)
53
+ case response.code.to_i
54
+ when 200, 202
55
+ Rails.logger.info "IndexNow: Successfully submitted URLs (#{response.code})"
56
+ true
57
+ when 400
58
+ Rails.logger.error "IndexNow: Bad request (400) - #{response.body}"
59
+ false
60
+ when 403
61
+ raise IndexNowError, "IndexNow: Forbidden (403) - Invalid or inaccessible API key. Verify key file at #{key_location_url}"
62
+ when 422
63
+ raise IndexNowError, "IndexNow: Unprocessable (422) - #{response.body}"
64
+ when 429
65
+ Rails.logger.warn "IndexNow: Rate limited (429)"
66
+ false
67
+ else
68
+ Rails.logger.error "IndexNow: Unexpected response (#{response.code}) - #{response.body}"
69
+ false
70
+ end
71
+ end
72
+
73
+ def domain
74
+ Helios::Sitemap.configuration.indexnow_domain || raise(IndexNowError, "indexnow_domain not configured")
75
+ end
76
+
77
+ def api_key
78
+ Helios::Sitemap.configuration.indexnow_api_key || raise(IndexNowError, "indexnow_api_key not configured")
79
+ end
80
+
81
+ def key_location_url
82
+ "https://#{domain}/#{api_key}.txt"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,5 @@
1
+ module Helios
2
+ module Sitemap
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ require "helios/sitemap/version"
2
+ require "helios/sitemap/engine"
3
+ require "helios/sitemap/configuration"
4
+ require "helios/sitemap/index_now_service"
5
+
6
+ module Helios
7
+ module Sitemap
8
+ class << self
9
+ def configuration
10
+ @configuration ||= Configuration.new
11
+ end
12
+
13
+ def configure
14
+ yield(configuration)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :helios_sitemap do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: helios-sitemap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jason Fleetwood-Boldt
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: sitemap_generator
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '6.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '6.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: aws-sdk-s3
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ description: Generate sitemaps with sitemap_generator, upload to S3 for ephemeral
55
+ disk systems, serve from your app at /sitemap.xml, and submit to IndexNow.
56
+ email:
57
+ - jason@heliosdev.shop
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - Rakefile
65
+ - app/assets/stylesheets/helios/sitemap/application.css
66
+ - app/controllers/helios/sitemap/application_controller.rb
67
+ - app/controllers/helios/sitemap/sitemap_controller.rb
68
+ - app/helpers/helios/sitemap/application_helper.rb
69
+ - app/jobs/helios/sitemap/application_job.rb
70
+ - app/jobs/helios/sitemap/refresh_job.rb
71
+ - app/mailers/helios/sitemap/application_mailer.rb
72
+ - app/models/helios/sitemap/application_record.rb
73
+ - app/views/layouts/helios/sitemap/application.html.erb
74
+ - config/routes.rb
75
+ - lib/helios/sitemap.rb
76
+ - lib/helios/sitemap/configuration.rb
77
+ - lib/helios/sitemap/engine.rb
78
+ - lib/helios/sitemap/index_now_service.rb
79
+ - lib/helios/sitemap/version.rb
80
+ - lib/tasks/helios/sitemap_tasks.rake
81
+ homepage: https://github.com/heliosdev/helios-sitemap
82
+ licenses:
83
+ - Nonstandard
84
+ metadata:
85
+ homepage_uri: https://github.com/heliosdev/helios-sitemap
86
+ source_code_uri: https://github.com/heliosdev/helios-sitemap
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.6.9
102
+ specification_version: 4
103
+ summary: Sitemap generation with S3 storage and IndexNow submission for Rails
104
+ test_files: []