seo_friendly 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e8ead4473d9c8e7226c5d09105070a7d2da6e00f
4
+ data.tar.gz: ce7b8a84efba8f92fbc403a1dc0fe2ff8321eca6
5
+ SHA512:
6
+ metadata.gz: 034b279d5ac1cc1f79110c1b07f0b0b60826262bcb8c2567cb8ae2f1cf81d6d95f014a5446e122d52716f45951a61b74dcb1b7f1d5e6093d0abed54abc17dd0d
7
+ data.tar.gz: f83ec0f07cc899190f081f06073a9b0c8a8ee4bce713f784bec9bb2fd7a95e3e0e212b483445f80031e3af77e137078a60f60fb66fa3c2cb31bf4ab31e506dd5
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ .idea/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in seo_friendly.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 buzhenko
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,70 @@
1
+ # SeoFriendly
2
+
3
+ SeoFriendly gem provides easy way to insert meta information (title, description, keywords) on almost all pages of your application.
4
+ Gem extract meta information for model from it's fields or from any source you specify.
5
+
6
+ ## Installation
7
+
8
+ ```ruby
9
+ gem 'seo_friendly'
10
+ ```
11
+
12
+ For fast gem integration run generator that copy migration files and default source config into your application.
13
+ Gem use monkey patching to add new methods to ActiveRecord and new view helpers.
14
+
15
+ $ rails generate seo_friendly:install
16
+
17
+ ## Usage
18
+
19
+ Create your own seo source class for any model and specify information which will be used to create meta tags.
20
+ Example:
21
+ ```ruby
22
+ # seo source class for Page model
23
+ class PageSeoSource < DefaultSeoSource
24
+
25
+ # title information will be extracted from title field of Page instance
26
+ def title
27
+ instance.title
28
+ end
29
+
30
+ # description will be always constant for Page
31
+ def description
32
+ 'Awesome pages of my beautiful application'
33
+ end
34
+
35
+ # Page model can have for example tags reference that you can use to generate keywords.
36
+ def keywords
37
+ instance.tags.join(' ,')
38
+ end
39
+
40
+ end
41
+ ```
42
+
43
+ ## Views
44
+ On views you can insert meta tags using helper
45
+
46
+ ```haml
47
+ = meta_tags(@page) %>
48
+ ```
49
+
50
+ ## Model
51
+ To update meta information every time when you update records add to model callback:
52
+ ```ruby
53
+ after_save :update_seo_attributes
54
+ ```
55
+
56
+ ## Rake
57
+ If you change model source and want to update seo information for all instances of this model you can use rake task
58
+
59
+ $ rake seo_friendly:regenerate[ModelName]
60
+
61
+ ## TODO
62
+ 2) Add tests
63
+ 6) WordsSplitter is too slow, replace it with regular expression (change)
64
+
65
+
66
+ 1. Fork it ( https://github.com/rossmari/seo_friendly/fork )
67
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
68
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
69
+ 4. Push to the branch (`git push origin my-new-feature`)
70
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,2 @@
1
+ Description:
2
+ Generates an application policy as a starting point for your application.
@@ -0,0 +1,23 @@
1
+ module SeoFriendly
2
+ module Generators
3
+ class InstallGenerator < ::Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ # TODO : rename method
9
+ def copy_default_seo_source
10
+ template 'default_seo_source.rb', 'app/seo_sources/default_seo_source.rb'
11
+ end
12
+
13
+ def copy_migrations
14
+ migration_template 'migrations/create_seo_params.rb', 'db/migrate/create_seo_params.rb'
15
+ end
16
+
17
+ def self.next_migration_number(dir)
18
+ Time.now.utc.strftime('%Y%m%d%H%M%S')
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ class DefaultSeoSource
2
+ attr_reader :instance
3
+
4
+ def initialize(instance)
5
+ @instance = instance
6
+
7
+ @title = title
8
+ @description = description
9
+ @keywords = keywords
10
+ end
11
+
12
+ def title
13
+ instance.title
14
+ rescue
15
+ 'Default SeoFriendly title'
16
+ end
17
+
18
+ def description
19
+ instance.description
20
+ rescue
21
+ 'Default SeoFriendly description'
22
+ end
23
+
24
+ def keywords
25
+ instance.keywords
26
+ rescue
27
+ 'Default SeoFriendly keywords'
28
+ end
29
+
30
+ end
31
+
@@ -0,0 +1,18 @@
1
+ class SeoFriendly < ActiveRecord::Migration
2
+
3
+ def change
4
+ create_table :seo_params do |t|
5
+ t.string :description
6
+ t.string :title
7
+ t.string :keywords
8
+
9
+ t.integer :searchable_id
10
+ t.string :searchable_type
11
+
12
+ t.timestamps null: false
13
+ end
14
+
15
+ add_index :seo_params, [ :searchable_id, :searchable_type ]
16
+
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ class SeoParam < ActiveRecord::Base
2
+
3
+ # === REFERENCES
4
+ belongs_to :searchable, polymorphic: true
5
+
6
+ end
@@ -0,0 +1,10 @@
1
+ require 'seo_friendly'
2
+ require 'rails'
3
+
4
+ module YourGem
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load 'tasks/rebuild_seo_params.rake'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ require 'action_view'
2
+ require 'seo_friendly/version'
3
+ require 'seo_friendly/seo_searcher'
4
+ require 'seo_friendly/exceptions'
5
+ require 'seo_friendly/renderer/meta_render'
6
+ require 'seo_friendly/view_helper'
7
+ require 'seo_friendly/model_helper'
8
+ require 'active_record'
9
+ require 'models/seo_param'
10
+
11
+
12
+ module SeoFriendly
13
+
14
+ ActionView::Base.send :include, SeoFriendly::ViewHelper
15
+ ActiveRecord::Base.send :include, SeoFriendly::ModelHelper
16
+
17
+ require 'railtie' if defined?(Rails)
18
+
19
+ end
@@ -0,0 +1,41 @@
1
+ #TODO RENAME TO META CONSTRUCTORS or META parsers
2
+ #TODO: add html escaping for data
3
+ require 'seo_friendly/words_splitter'
4
+
5
+ module SeoFriendly
6
+
7
+ # Recommended title tag length: up to 70 characters, 10 words.
8
+ class TitleConstructor
9
+
10
+ def self.extract(object)
11
+ WordsSplitter.split(object, 70, 10)
12
+ end
13
+
14
+ end
15
+
16
+ class DescriptionConstructor
17
+
18
+ OMISSION = '...'
19
+
20
+ # Recommended description tag length: up to 160 characters.
21
+ # In my document maximal words count is 200
22
+ def self.extract(object)
23
+ # TODO : remove maximal words count some how
24
+ description = WordsSplitter.split(object, 200, 100)
25
+ unless description.end_with?('.')
26
+ description << OMISSION
27
+ end
28
+ return description
29
+ end
30
+
31
+ end
32
+
33
+ class KeyWordsConstructor
34
+
35
+ def self.extract(object)
36
+ WordsSplitter.split(object, 255, 20)
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,33 @@
1
+ require 'seo_friendly/constructors/meta_constructors'
2
+ module SeoFriendly
3
+
4
+ class DataExtractor
5
+
6
+ attr_reader :source, :title, :description, :keywords
7
+
8
+ # a little bit of metaprogramming
9
+ EXTRACTORS = { title: TitleConstructor, description: DescriptionConstructor, keywords: KeyWordsConstructor }
10
+
11
+ def initialize(source)
12
+ @source = source
13
+ extract_information
14
+ end
15
+
16
+ def seo_attributes
17
+ { title: title, description: description, keywords: keywords }
18
+ end
19
+
20
+ private
21
+
22
+ def extract_information
23
+ EXTRACTORS.each do |attribute_name, extractor_class|
24
+ instance_variable_set("@#{attribute_name}", extractor_class.send(:extract, source.send(attribute_name)))
25
+ end
26
+ rescue => exception
27
+ raise ExtractDataFromSourceError.new(source.class.name, exception.message)
28
+ end
29
+
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,51 @@
1
+ module SeoFriendly
2
+
3
+ class SourceNotDefinedError < StandardError
4
+
5
+ MESSAGE = 'Unable to find seo rule %{policy_name} for %{object} class'
6
+
7
+ attr_reader :object
8
+
9
+ def initialize(policy, object)
10
+ super( MESSAGE % { policy_name: policy, object: object} )
11
+ @object = object
12
+ end
13
+
14
+ end
15
+
16
+ class UnknownSeoAttributeError < StandardError
17
+
18
+ MESSAGE = 'Unknown seo attribute method \'%{seo_method}\' for field \'%{seo_field}\'.'
19
+
20
+ attr_reader :object
21
+
22
+ def initialize(seo_field, seo_method)
23
+ super( MESSAGE % { seo_field: seo_field, seo_method: seo_method } )
24
+ @object = object
25
+ end
26
+
27
+ end
28
+
29
+ class UnknownSeoModeError < StandardError
30
+
31
+ MESSAGE = 'Unknown seo mode value %{seo_mode}'
32
+
33
+ attr_reader :object
34
+
35
+ def initialize(mode)
36
+ super( MESSAGE % { seo_mode: mode} )
37
+ @object = object
38
+ end
39
+
40
+ end
41
+
42
+ class ExtractDataFromSourceError < StandardError
43
+ MESSAGE = 'Failed to extract data from source %{source_name}. '
44
+ attr_reader :object
45
+ def initialize(source_name, stack_message)
46
+ super( MESSAGE % { source_name: source_name} << stack_message )
47
+ @object = object
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,12 @@
1
+ require 'seo_friendly/seo_updater'
2
+ module SeoFriendly
3
+
4
+ module ModelHelper
5
+
6
+ def update_seo_attributes
7
+ SeoUpdater.new(self).update
8
+ end
9
+
10
+ end
11
+
12
+ end
@@ -0,0 +1,15 @@
1
+ module SeoFriendly
2
+
3
+ class MetaRenderer
4
+
5
+ def self.render(container, context)
6
+ tags = ''
7
+ tags << context.content_tag(:meta, nil, content: container.title, name: 'title')
8
+ tags << context.content_tag(:meta, nil, content: container.description, name: 'description')
9
+ tags << context.content_tag(:meta, nil, content: container.keywords, name: 'keywords')
10
+ context.raw(tags)
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,13 @@
1
+ require 'seo_friendly/source_searcher'
2
+ require 'seo_friendly/data_extractor'
3
+ require 'seo_friendly/seo_params_processor'
4
+
5
+ module SeoFriendly
6
+ class SeoCreator < SeoParamsProcessor
7
+
8
+ def create
9
+ SeoParam.create(extracted_seo_attributes.merge(searchable: instance))
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ require 'seo_friendly/source_searcher'
2
+ require 'seo_friendly/data_extractor'
3
+
4
+ module SeoFriendly
5
+ class SeoParamsProcessor
6
+
7
+ attr_reader :instance, :raw_data_source, :extracted_seo_attributes
8
+
9
+ def initialize(instance)
10
+ @instance = instance
11
+ @raw_data_source = create_data_source
12
+ @extracted_seo_attributes = extract_seo_attributes
13
+ end
14
+
15
+ private
16
+
17
+ def create_data_source
18
+ source_class = SourceSearcher.new(instance).find_source
19
+ source_class.new(instance)
20
+ end
21
+
22
+ def extract_seo_attributes
23
+ extractor = DataExtractor.new(raw_data_source)
24
+ extractor.seo_attributes
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ require 'seo_friendly/seo_creator'
2
+ module SeoFriendly
3
+
4
+ class SeoSearcher
5
+
6
+ attr_reader :id, :type
7
+
8
+ def initialize(searchable_id, searchable_type)
9
+ @id = searchable_id
10
+ @type = searchable_type
11
+ end
12
+
13
+ def search
14
+ SeoParam.where(searchable_type: type, searchable_id: id).first
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,14 @@
1
+ require 'seo_friendly/source_searcher'
2
+ require 'seo_friendly/data_extractor'
3
+ require 'seo_friendly/seo_params_processor'
4
+
5
+ module SeoFriendly
6
+ class SeoUpdater < SeoParamsProcessor
7
+
8
+ def update
9
+ seo_record = SeoSearcher.new(instance.id, instance.class.name).search
10
+ seo_record.update_attributes(extracted_seo_attributes)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ module SeoFriendly
2
+ class SourceSearcher
3
+
4
+ attr_reader :object
5
+
6
+ def initialize(model)
7
+ @object = model
8
+ end
9
+
10
+ def find_source
11
+ source or raise SourceNotDefinedError.new(find, object)
12
+ end
13
+
14
+ private
15
+
16
+ def source
17
+ klass = find
18
+ klass = klass.constantize if klass.is_a?(String)
19
+ klass
20
+ rescue NameError
21
+ nil
22
+ end
23
+
24
+ def find
25
+ klass = if object.respond_to?(:model_name)
26
+ object.model_name
27
+ elsif object.class.respond_to?(:model_name)
28
+ object.class.model_name
29
+ elsif object.is_a?(Class)
30
+ object
31
+ elsif object.is_a?(Symbol)
32
+ object.to_s.classify
33
+ else
34
+ object.class
35
+ end
36
+ "#{klass}SeoSource"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module SeoFriendly
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,19 @@
1
+ require 'seo_friendly/seo_searcher'
2
+ require 'seo_friendly/seo_creator'
3
+
4
+ module SeoFriendly
5
+
6
+ module ViewHelper
7
+
8
+ def meta_tags(instance)
9
+ container = SeoSearcher.new(instance.id, instance.class.name).search || SeoCreator.new(instance).create
10
+ MetaRenderer.render(container, self)
11
+ end
12
+
13
+ def meta_container(instance)
14
+ SeoSearcher.new(instance.id, instance.class.name).search || SeoCreator.new(instance).create
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,26 @@
1
+ module SeoFriendly
2
+ class WordsSplitter
3
+
4
+ def self.split(object, maximal_length, maximal_words_count)
5
+ if object.length <= maximal_length && words_count(object) <= maximal_words_count
6
+ return object
7
+ else
8
+ object = reduce_string(object)
9
+ split(object, maximal_length, maximal_words_count)
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def self.words_count(object)
16
+ object.split.count
17
+ end
18
+
19
+ def self.reduce_string(object)
20
+ array = object.split
21
+ array.delete(array.last)
22
+ array.join(' ')
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ namespace :seo_friendly do
2
+
3
+ desc 'Regenerate seo params for chosen model'
4
+ task :regenerate, [:model_name] => :environment do |t, args|
5
+
6
+ unless args[:model_name]
7
+ abort('WARNING! Set model name parametr to execute this rake task')
8
+ end
9
+
10
+ model = args[:model_name].capitalize.constantize
11
+
12
+ model.find_each do |instance|
13
+
14
+ instance.update_seo_attributes
15
+ puts "Update seo attributes for #{instance.class.name} with ID #{instance.id}"
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'seo_friendly/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "seo_friendly"
8
+ spec.version = SeoFriendly::VERSION
9
+ spec.authors = ["buzhenko"]
10
+ spec.email = ["roman-bujenko@yandex.ru"]
11
+ spec.summary = %q{Seo }
12
+ spec.description = %q{Seo}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "activerecord", "~> 4.2.0"
24
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: seo_friendly
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - buzhenko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 4.2.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 4.2.0
55
+ description: Seo
56
+ email:
57
+ - roman-bujenko@yandex.ru
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - Gemfile
64
+ - LICENSE.txt
65
+ - README.md
66
+ - Rakefile
67
+ - lib/generators/seo_friendly/install/USAGE
68
+ - lib/generators/seo_friendly/install/install_generator.rb
69
+ - lib/generators/seo_friendly/install/templates/default_seo_source.rb
70
+ - lib/generators/seo_friendly/install/templates/migrations/create_seo_params.rb
71
+ - lib/models/seo_param.rb
72
+ - lib/railtie.rb
73
+ - lib/seo_friendly.rb
74
+ - lib/seo_friendly/constructors/meta_constructors.rb
75
+ - lib/seo_friendly/data_extractor.rb
76
+ - lib/seo_friendly/exceptions.rb
77
+ - lib/seo_friendly/model_helper.rb
78
+ - lib/seo_friendly/renderer/meta_render.rb
79
+ - lib/seo_friendly/seo_creator.rb
80
+ - lib/seo_friendly/seo_params_processor.rb
81
+ - lib/seo_friendly/seo_searcher.rb
82
+ - lib/seo_friendly/seo_updater.rb
83
+ - lib/seo_friendly/source_searcher.rb
84
+ - lib/seo_friendly/version.rb
85
+ - lib/seo_friendly/view_helper.rb
86
+ - lib/seo_friendly/words_splitter.rb
87
+ - lib/tasks/rebuild_seo_params.rake
88
+ - seo_friendly.gemspec
89
+ homepage: ''
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.4.5
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Seo
113
+ test_files: []