ar_sitemapper 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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