ar_sitemapper 1.0.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.
Files changed (68) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +9 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/Changelog.rdoc +28 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +98 -0
  8. data/LICENSE +24 -0
  9. data/README.rdoc +176 -0
  10. data/Rakefile +23 -0
  11. data/ar_sitemapper.gemspec +26 -0
  12. data/install.rb +5 -0
  13. data/lib/ar_sitemapper.rb +11 -0
  14. data/lib/sitemapper/active_record/builder.rb +54 -0
  15. data/lib/sitemapper/engine.rb +26 -0
  16. data/lib/sitemapper/generator.rb +100 -0
  17. data/lib/sitemapper/index.rb +84 -0
  18. data/lib/sitemapper/loader.rb +39 -0
  19. data/lib/sitemapper/pinger.rb +21 -0
  20. data/lib/sitemapper/sitemap.rb +22 -0
  21. data/lib/sitemapper/urlset.rb +76 -0
  22. data/lib/sitemapper/version.rb +5 -0
  23. data/lib/tasks/sitemapper.rake +26 -0
  24. data/templates/sitemaps.yml +34 -0
  25. data/test/dummy/README.rdoc +261 -0
  26. data/test/dummy/Rakefile +7 -0
  27. data/test/dummy/app/assets/javascripts/application.js +15 -0
  28. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  29. data/test/dummy/app/controllers/application_controller.rb +3 -0
  30. data/test/dummy/app/helpers/application_helper.rb +2 -0
  31. data/test/dummy/app/mailers/.gitkeep +0 -0
  32. data/test/dummy/app/models/.gitkeep +0 -0
  33. data/test/dummy/app/views/layouts/application.html.erb +24 -0
  34. data/test/dummy/config.ru +4 -0
  35. data/test/dummy/config/application.rb +50 -0
  36. data/test/dummy/config/boot.rb +10 -0
  37. data/test/dummy/config/database.yml +25 -0
  38. data/test/dummy/config/environment.rb +5 -0
  39. data/test/dummy/config/environments/development.rb +24 -0
  40. data/test/dummy/config/environments/production.rb +69 -0
  41. data/test/dummy/config/environments/test.rb +37 -0
  42. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  43. data/test/dummy/config/initializers/inflections.rb +15 -0
  44. data/test/dummy/config/initializers/mime_types.rb +5 -0
  45. data/test/dummy/config/initializers/secret_token.rb +7 -0
  46. data/test/dummy/config/initializers/session_store.rb +8 -0
  47. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  48. data/test/dummy/config/locales/en.yml +5 -0
  49. data/test/dummy/config/routes.rb +2 -0
  50. data/test/dummy/config/sitemaps.yml +36 -0
  51. data/test/dummy/db/schema.rb +74 -0
  52. data/test/dummy/lib/assets/.gitkeep +0 -0
  53. data/test/dummy/log/.gitkeep +0 -0
  54. data/test/dummy/public/404.html +26 -0
  55. data/test/dummy/public/422.html +26 -0
  56. data/test/dummy/public/500.html +25 -0
  57. data/test/dummy/public/favicon.ico +0 -0
  58. data/test/dummy/script/rails +6 -0
  59. data/test/support/app/models/foo_bar.rb +24 -0
  60. data/test/test_helper.rb +13 -0
  61. data/test/unit/engine_test.rb +19 -0
  62. data/test/unit/generator_test.rb +14 -0
  63. data/test/unit/loader_test.rb +21 -0
  64. data/test/unit/map_test.rb +9 -0
  65. data/test/unit/pinger_test.rb +11 -0
  66. data/test/unit/sitemapper_test.rb +22 -0
  67. data/test/unit/urlset_test.rb +9 -0
  68. metadata +221 -0
@@ -0,0 +1,26 @@
1
+ require "rails"
2
+ require 'sitemapper/loader'
3
+
4
+ module AegisNet
5
+ module Sitemapper
6
+
7
+ class Engine < Rails::Engine
8
+
9
+ initializer 'ar_sitemapper.load_app_root' do |app|
10
+ AegisNet::Sitemapper.sitemap_file ||= File.join(app.root, "config", "sitemaps.yml")
11
+ end
12
+
13
+ initializer 'ar_sitemapper.hook_into_active_record' do
14
+ ActiveSupport.on_load(:active_record) do
15
+ ::ActiveRecord::Base.send :include, AegisNet::Sitemapper::ActiveRecord::Builder
16
+ end
17
+ end
18
+
19
+ rake_tasks do
20
+ load 'tasks/sitemapper.rake'
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,100 @@
1
+ module AegisNet
2
+ module Sitemapper
3
+ class Generator
4
+
5
+ require "zlib"
6
+
7
+ VALID_GENERATOR_OPTIONS = [:file, :filename, :gzip, :xmlns]
8
+
9
+ # Generates an XML Sitemap file with +entries+. The output is written
10
+ # to a file if a filename is given or to stdout otherwise. Expects a
11
+ # block.
12
+ #
13
+ # === Parameters
14
+ # * +entries+: Enumerable to iterate through
15
+ # * +options+: an optional Hash. See below for supported options
16
+ #
17
+ # === Available Options
18
+ # * +:file+: full path to output file. If +:filename+ ends with .gz, Gzip-compression is activated
19
+ # * +:gzip+: force GZip compression if set to +true+
20
+ # * +:xmlns+: XML namespace to use, defaults to http://www.sitemaps.org/schemas/sitemap/0.9
21
+ #
22
+ # === Example
23
+ # sites = [
24
+ # { :url => "http://example.com/your/static/content1.html", :freq => "always", :prio => "1.0" },
25
+ # { :url => "http://example.com/your/static/content2.html", :freq => "monthly", :prio => "0.3" },
26
+ # ]
27
+ #
28
+ # AegisNet::Sitemapper::Generator.create(sites) do |site, xml|
29
+ # xml.loc site[:url]
30
+ # xml.changefreq site[:freq]
31
+ # xml.priority site[:prio]
32
+ # end
33
+ #
34
+ def self.create entries, options = {}, &block
35
+ if block_given?
36
+ options.symbolize_keys!
37
+ options.assert_valid_keys(VALID_GENERATOR_OPTIONS)
38
+ xmlns = options[:xmlns] || "http://www.sitemaps.org/schemas/sitemap/0.9"
39
+ gzip = options[:gzip] || /\.gz$/.match(options[:file])
40
+ filename = options[:file] ? options[:file].gsub(/\.gz$/, '') : nil
41
+
42
+ if entries.size > 50_000
43
+ part_number = 0
44
+ entries.each_slice(50_000) do |part|
45
+ part_number = part_number.next
46
+ part_fn = filename.gsub('.xml', ".#{part_number}.xml")
47
+
48
+ create_one_sitemap(part, xmlns, part_fn, gzip, &block)
49
+ end
50
+ else
51
+ create_one_sitemap(entries, xmlns, filename, gzip, &block)
52
+ end
53
+ end
54
+ end
55
+
56
+ # Infer full local path and sitemap filename by class name. Adds .xml.gz
57
+ #
58
+ # === Parameters
59
+ # * +klass+: class name
60
+ def self.default_filename(klass)
61
+ config = AegisNet::Sitemapper::Loader.load_config
62
+ File.join(config[:local_path], "sitemap_#{klass.to_s.underscore.pluralize}.xml.gz") if config[:local_path]
63
+ end
64
+
65
+ def self.create_necessary_directories(filename)
66
+ FileUtils.mkpath( File.dirname(filename) )
67
+ end
68
+
69
+ def self.create_one_sitemap(entries, xmlns, filename, gzip, &block)
70
+ write_one_sitemap(
71
+ generate_one_sitemap(entries, xmlns, &block),
72
+ filename,
73
+ gzip
74
+ )
75
+ end
76
+
77
+ def self.generate_one_sitemap(entries, xmlns, &block)
78
+ xml = Builder::XmlMarkup.new(:indent => 2)
79
+ xml.instruct!
80
+ xml.urlset "xmlns" => xmlns do
81
+ entries.each do |entry|
82
+ xml.url { block.call(entry, xml) } rescue nil # TODO handle me / pass upwards
83
+ end
84
+ end
85
+ xml
86
+ end
87
+
88
+ def self.write_one_sitemap(xml, filename, gzip)
89
+ # Either write to file or to stdout
90
+ if filename
91
+ create_necessary_directories(filename)
92
+ File.open(filename, "w") { |file| file.puts xml.target! }
93
+ Zlib::GzipWriter.open("#{filename}.gz") {|gz| gz.write xml.target! } if gzip
94
+ else
95
+ $stdout.puts xml.target!
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,84 @@
1
+ module AegisNet # :nodoc:
2
+ module Sitemapper # :nodoc:
3
+ # :doc:
4
+
5
+ class Index
6
+
7
+ attr_reader :host, :sitemaps
8
+
9
+ def initialize(options = {})
10
+ options.symbolize_keys!
11
+ @sitemaps = []
12
+
13
+ @config = AegisNet::Sitemapper::Loader.load_config
14
+ @host = @config[:default_host]
15
+ @file = File.join("#{@config[:local_path]}", @config[:index]["sitemapfile"])
16
+
17
+ @static = @config[:static]
18
+ @models = @config[:models]
19
+ @includes = @config[:index]["includes"]
20
+
21
+ # Validate all variables
22
+ raise(ArgumentError, "No filename specified") if @file.nil?
23
+
24
+ # Static Sitemap
25
+ if @static
26
+ sitemap_options = {:loc => @static["sitemapfile"], :lastmod => @static["lastmod"]}
27
+ @sitemaps << AegisNet::Sitemapper::Urlset.new(sitemap_options)
28
+ end
29
+
30
+ # Include additional sitemaps
31
+ @includes.each do |sitemap|
32
+ sitemap_options = {:loc => sitemap["loc"] }
33
+ sitemap_options.merge!(:lastmod => sitemap["lastmod"]) if sitemap["lastmod"]
34
+ @sitemaps << AegisNet::Sitemapper::Sitemap.new(sitemap_options)
35
+ end
36
+
37
+ @models.each do |sitemap|
38
+ klass = sitemap.first.camelize.constantize
39
+ count = klass.count
40
+
41
+ order_opts = {}
42
+ order_opts = { :order => :created_at } if klass.column_names.include?("created_at")
43
+ lastmod = sitemap.last["lastmod"] || klass.last(order_opts).created_at
44
+
45
+ if count <= 50_000
46
+ sitemap_options = {:loc => sitemap.last["sitemapfile"], :lastmod => lastmod}
47
+ @sitemaps << AegisNet::Sitemapper::Urlset.new(sitemap_options)
48
+ else
49
+ 1.upto( (count / 50_000.0).ceil ) do |part_number|
50
+ sitemap_options = {
51
+ :loc => sitemap.last["sitemapfile"].gsub("xml", "#{part_number}.xml"),
52
+ :lastmod => lastmod
53
+ }
54
+ @sitemaps << AegisNet::Sitemapper::Urlset.new(sitemap_options)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def self.create!(options = {})
61
+ index = self.new(options)
62
+ index.create!
63
+ end
64
+
65
+ def create!
66
+ xml = Builder::XmlMarkup.new(:indent => 2)
67
+ xml.instruct!
68
+
69
+ xml.sitemapindex "xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9" do
70
+
71
+ @sitemaps.each do |sitemap|
72
+ location = sitemap.loc.gsub(/^\//, '')
73
+ xml.sitemap do
74
+ xml.loc "http://#{@host}/#{location}"
75
+ xml.lastmod sitemap.lastmod.to_date if sitemap.lastmod rescue nil # TODO handle properly
76
+ end
77
+ end
78
+ end
79
+ File.open(@file, "w") { |file| file.puts xml.target! }
80
+ end
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,39 @@
1
+ require 'sitemapper/generator'
2
+ require 'sitemapper/sitemap'
3
+ require 'sitemapper/urlset'
4
+ require 'sitemapper/index'
5
+ require 'sitemapper/pinger'
6
+ require 'sitemapper/active_record/builder'
7
+
8
+ module AegisNet
9
+ module Sitemapper
10
+
11
+ class Loader
12
+ # Loads the sitemap configuration from Rails.root/config/sitemap.yml
13
+ def self.load_config
14
+ # TODO verify file integrity
15
+ erb = ERB.new(File.read(AegisNet::Sitemapper.sitemap_file))
16
+ AegisNet::Sitemapper.configuration ||= HashWithIndifferentAccess.new(YAML.load(StringIO.new(erb.result)))
17
+ end
18
+
19
+ # Interprets +string+ as Ruby code representing a Proc and exectutes it.
20
+ #
21
+ # === Parameters
22
+ # * +string+: Ruby (Proc) code to be executed
23
+ # All other arguments will be passed to the Proc
24
+ #
25
+ # === Examples
26
+ # AegisNet::Sitemapper::Loader.proc_loader('Proc.new{"foo"}')
27
+ # => "foo"
28
+ #
29
+ # proc_str = 'Proc.new{|n| n}'
30
+ # AegisNet::Sitemapper::Loader.proc_loader(proc_str, "hello world")
31
+ # => "hello world"
32
+ def self.proc_loader(string, *args)
33
+ # TODO lambdas
34
+ eval(string).call(*args)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ module AegisNet # :nodoc:
2
+ module Sitemapper # :nodoc:
3
+ # :doc:
4
+
5
+ class Pinger
6
+
7
+ require "net/http"
8
+
9
+ def self.ping!
10
+ config = AegisNet::Sitemapper::Loader.load_config
11
+ if config[:ping] and config[:default_host] and config[:index]
12
+ config[:pings].each do |ping_url|
13
+ url = ping_url + config[:default_host] + "/" + config[:index]["sitemapfile"]
14
+ Net::HTTP.get_response(URI.parse(url)) rescue nil
15
+ end
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ module AegisNet # :nodoc:
2
+ module Sitemapper # :nodoc:
3
+ # :doc:
4
+ class Sitemap
5
+ attr_reader :lastmod, :loc
6
+
7
+ def initialize(options = {})
8
+ options.symbolize_keys!
9
+ options.assert_valid_keys(:changefreq, :lastmod, :loc, :priority)
10
+ @changefreq = options[:changefreq] || "weekly"
11
+ @lastmod = options[:lastmod]
12
+ @loc = options[:loc]
13
+ @priority = options[:priority] || 0.5
14
+ end
15
+
16
+ def changefreq(freq = nil); freq ? @changefreq = freq : @changefreq; end
17
+ def lastmod(lastmod = nil); lastmod ? @lastmod = lastmod : @lastmod; end
18
+ def loc(loc = nil); loc ? @loc = loc : @loc; end
19
+ def priority(prio = nil); prio ? @priority = prio : @priority; end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,76 @@
1
+ module AegisNet # :nodoc:
2
+ module Sitemapper # :nodoc:
3
+ # :doc:
4
+
5
+ class Urlset < AegisNet::Sitemapper::Sitemap
6
+
7
+ def create!
8
+ xml = Builder::XmlMarkup.new(:indent => 2)
9
+ xml.instruct!
10
+
11
+ xml.urlset "xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9" do
12
+
13
+ @sitemaps.each do |sitemap|
14
+ location = sitemap.loc.gsub(/^\//, '')
15
+ xml.url do
16
+ xml.loc "http://#{@host}/#{location}"
17
+ xml.lastmod sitemap.lastmod if sitemap.lastmod
18
+ xml.changefreq sitemap.changefreq
19
+ xml.priority sitemap.priority
20
+ end
21
+ end
22
+
23
+ end
24
+ File.open(@file, "w") { |file| file.puts xml.target! }
25
+ end
26
+
27
+ # Short-hand for Urlset#new and Urlset#create!
28
+ def self.create!(options = {})
29
+ sitemap = self.new(options)
30
+ sitemap.create!
31
+ end
32
+
33
+ # Generate Urlset sitemaps listed in sitemaps.yml
34
+ def self.build_all!
35
+ config = AegisNet::Sitemapper::Loader.load_config
36
+
37
+ # Generate sitemaps for AR Models dynamically by yml instructions
38
+ if config[:models]
39
+ config[:models].each do |ar_map|
40
+ opts = ar_map.last
41
+ klass = ar_map.first.camelize.constantize
42
+
43
+ build_opts = { :file => File.join("#{config[:local_path]}", opts["sitemapfile"]) }
44
+ build_opts.merge!( :conditions => opts["conditions"]) if opts["conditions"]
45
+
46
+ scope = opts["scope"].present? ? "#{opts["scope"]}" : :all
47
+
48
+ klass.build_sitemap scope, build_opts do |object, xml|
49
+ if opts["loc"].starts_with?("Proc")
50
+ xml.loc AegisNet::Sitemapper::Loader.proc_loader(opts["loc"], object)
51
+ else
52
+ xml.loc opts["loc"]
53
+ end
54
+ xml.lastmod object.updated_at.to_date
55
+ xml.changefreq opts["changefreq"] || "weekly"
56
+ xml.priority opts["priority"] || 0.5
57
+ end
58
+ end
59
+ end
60
+
61
+ # Find misc. sitemap data and generate a single static one
62
+ if config[:static]
63
+ entries = config[:static]["urlset"]
64
+ file = File.join("#{config[:local_path]}", config[:static]["sitemapfile"])
65
+ AegisNet::Sitemapper::Generator.create(entries, :file => file) do |entry, xml|
66
+ xml.loc entry["loc"]
67
+ xml.lastmod entry["lastmod"] if entry["lastmod"]
68
+ xml.changefreq entry["changefreq"] if entry["changefreq"]
69
+ xml.priority entry["priority"] || 0.5
70
+ end
71
+ end
72
+
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,5 @@
1
+ module AegisNet
2
+ module Sitemapper
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ namespace :sitemapper do
3
+
4
+ desc "Rebuilds all sitemaps"
5
+ task :rebuild => [:environment, :build_from_config]
6
+
7
+ desc "Notifies search-engines about the index-sitemap"
8
+ task :ping => :environment do
9
+ ActiveRecord::Migration.say_with_time "Pinging searchengines" do
10
+ AegisNet::Sitemapper::Pinger.ping!
11
+ end
12
+ end
13
+
14
+ desc "Rebuild everything from config/sitemapper.yml"
15
+ task :build_from_config => :environment do
16
+ include Rails.application.routes.url_helpers
17
+ config = AegisNet::Sitemapper::Loader.load_config
18
+ default_url_options[:host] = config["default_host"]
19
+
20
+ ActiveRecord::Migration.say_with_time "Rebuilding sitemaps from #{AegisNet::Sitemapper.sitemap_file}" do
21
+ AegisNet::Sitemapper::Urlset.build_all!
22
+ AegisNet::Sitemapper::Index.create!
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,34 @@
1
+ default_host: "www.example.com"
2
+ local_path: <%= File.join Rails.root, "public", "sitemaps" %>
3
+ ping: true
4
+
5
+ index:
6
+ sitemapfile: "sitemap_index.xml"
7
+ includes:
8
+ -
9
+ loc: foo_sitemap.xml
10
+ static:
11
+ sitemapfile: "sitemapfoobar.xml.gz"
12
+ urlset:
13
+ -
14
+ loc: "http://www.example.com/static/page"
15
+ changefreq: weekly
16
+ priority: 0.1
17
+ -
18
+ loc: "http://www.example.com/important/static/page"
19
+ changefreq: daily
20
+ priority: 1.0
21
+
22
+ models:
23
+ foo_bar:
24
+ conditions: "foo = bar"
25
+ sitemapfile: sitemap_foo_bars.xml.gz
26
+ loc: Proc.new {|o| foo_bar_url(o) }
27
+ changefreq: weekly
28
+ priority: 0.7
29
+
30
+ pings:
31
+ - http://submissions.ask.com/ping?sitemap=
32
+ - http://www.google.com/webmasters/sitemaps/ping?sitemap=
33
+ - http://search.yahooapis.com/SiteExplorerService/V1/updateNotification?appid=YahooDemo&url=
34
+ - http://www.bing.com/webmaster/ping.aspx?siteMap