diff --git a/.gitignore b/.gitignore index f35b9ea9..64f5e1ff 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ set_aws_credentials.sh # Ignore map images uploaded to Locations /locations +public/sitemap.xml.gz diff --git a/Gemfile b/Gemfile index 7e2d43b5..ebd700ce 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,9 @@ gem 'jquery-rails' gem 'jquery-ui-rails' gem 'rails-jquery-autocomplete' +# SEO +gem 'meta-tags' + # Smarts # gem 'serendipitous', :path => "~/Code/indent/serendipitous-gem" gem 'serendipitous', git: 'git://github.com/indentlabs/serendipitous-gem.git' diff --git a/Gemfile.lock b/Gemfile.lock index 9fe868ff..83886477 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -164,6 +164,8 @@ GEM railties (>= 3.2) medium-editor-rails (2.2.0) railties (>= 3.0) + meta-tags (2.2.0) + actionpack (>= 3.2.0) method_source (0.8.2) mime-types (3.0) mime-types-data (~> 3.2015) @@ -317,6 +319,7 @@ DEPENDENCIES jquery-ui-rails material_icons medium-editor-rails + meta-tags paperclip (~> 4.2.0) pg pry diff --git a/README.rdoc b/README.rdoc index 8e9eed25..cd103b01 100644 --- a/README.rdoc +++ b/README.rdoc @@ -61,6 +61,10 @@ Install gems bundle install +Run initial database migrations + + rake db:migrate + Optional: To enable the uploading and editing of images (used in Locations management, etc), you will need to create a file named set_aws_credentials.rb with the following content: [[ $_ != $0 ]] && echo "Ready to run your server!" || echo "This script needs to be sourced!" diff --git a/app/models/user.rb b/app/models/user.rb index 11d01ba5..5110ea7f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -16,6 +16,25 @@ class User < ActiveRecord::Base has_many :magics has_many :universes + # as_json creates a hash structure, which you then pass to ActiveSupport::json.encode to actually encode the object as a JSON string. + # This is different from to_json, which converts it straight to an escaped JSON string, + # which is undesireable in a case like this, when we want to modify it + def as_json(options={}) + options[:except] ||= blacklisted_attributes + super(options) + end + + # Returns this object as an escaped JSON string + def to_json(options={}) + options[:except] ||= blacklisted_attributes + super(options) + end + + def to_xml(options={}) + options[:except] ||= blacklisted_attributes + super(options) + end + def content { characters: characters, @@ -37,4 +56,18 @@ class User < ActiveRecord::Base universes.length ].sum end + + private + + # Attributes that are non-public, and should be blacklisted from any public + # export (ex. in the JSON api, or SEO meta info about the user) + def blacklisted_attributes + [ + :password_digest, + :old_password, + :encrypted_password, + :reset_password_token, + :email + ] + end end diff --git a/app/views/characters/show.html.erb b/app/views/characters/show.html.erb index b14974f0..a7b3eda3 100644 --- a/app/views/characters/show.html.erb +++ b/app/views/characters/show.html.erb @@ -1 +1,14 @@ +<%# to_json will escape any values into unicode escape sequences, so we can call html_safe %> + + + <%= render partial: 'content/show', locals: { content: @content } %> diff --git a/app/views/content/_show.html.erb b/app/views/content/_show.html.erb index 15d84723..8f12979a 100644 --- a/app/views/content/_show.html.erb +++ b/app/views/content/_show.html.erb @@ -1,4 +1,14 @@ -<% title @content.name %> +<% if @content.present? && @content.respond_to?(:as_jsonld) %> + +<% end %> + +<% +set_meta_tags title: content.name, description: content.description +%> + + <% content_for :sidebar_top do %> <%= render partial: 'cards/serendipitous/content_question', locals: { question: @question, content: @content } %> diff --git a/app/views/items/show.html.erb b/app/views/items/show.html.erb index b14974f0..4c296455 100644 --- a/app/views/items/show.html.erb +++ b/app/views/items/show.html.erb @@ -1 +1,12 @@ + + <%= render partial: 'content/show', locals: { content: @content } %> diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb index b3d952cf..cb77e1fe 100644 --- a/app/views/layouts/_footer.html.erb +++ b/app/views/layouts/_footer.html.erb @@ -19,7 +19,7 @@
Privacy Policy / - Source Code + Source Code
diff --git a/app/views/layouts/_ganalytics.html.erb b/app/views/layouts/_ganalytics.html.erb index 839a9917..5bcc7fd9 100644 --- a/app/views/layouts/_ganalytics.html.erb +++ b/app/views/layouts/_ganalytics.html.erb @@ -2,7 +2,7 @@ var _gaq = _gaq || []; _gaq.push(['_setAccount', 'UA-39217500-1']); - _gaq.push(['_setDomainName', 'indentapp.com']); + _gaq.push(['_setDomainName', 'notebook.ai']); _gaq.push(['_trackPageview']); (function() { @@ -11,4 +11,4 @@ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); - \ No newline at end of file + diff --git a/app/views/layouts/_seo.html.erb b/app/views/layouts/_seo.html.erb new file mode 100644 index 00000000..11b13053 --- /dev/null +++ b/app/views/layouts/_seo.html.erb @@ -0,0 +1,32 @@ + +<%= +# This belongs inside the tag. +# Default values and pointers here only. +# Most content should be set in the pages themselves. +# Any values set here can be overridden. + +# Default & site-wide values + +display_meta_tags site: 'Notebook', +publisher: 'https://plus.google.com/118076966717703203223', +image_src: image_url('card-headers/hero.png'), +description: 'Notebook is a set of tools for writers, game designers, and roleplayers to create magnificent universes — and everything within them.', +# Recommended keywords tag length: up to 255 characters, 20 words. +keywords: %w[writing author nanowrimo novel character fiction fantasy universe creative dnd roleplay larp game design], +og: { + title: :title, + site_name: 'Notebook', + url: request.url, + image: :image_src, + description: :description, +}, +twitter: { + card: 'summary', + title: :title, + site: '@IndentLabs', + image: :image_src, + url: request.url, + description: :description +} +%> + diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 5acd21df..f181e22b 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,7 +1,6 @@ - <%= content_for?(:title) ? yield(:title) : 'Notebook' %> <%= stylesheet_link_tag 'application' %> <%= javascript_include_tag 'application' %> <%= csrf_meta_tags %> @@ -10,6 +9,9 @@ + + <%# is set in _seo.html.erb %> + <%= render 'layouts/seo' %> </head> <body> @@ -29,7 +31,6 @@ <%= render 'layouts/ganalytics' %> - <a href="https://plus.google.com/118076966717703203223" rel="publisher"></a> </main> <%= render 'layouts/footer' %> diff --git a/app/views/locations/show.html.erb b/app/views/locations/show.html.erb index b14974f0..96ba891e 100644 --- a/app/views/locations/show.html.erb +++ b/app/views/locations/show.html.erb @@ -1 +1,12 @@ +<script type="application/ld+json"> +<% +content_jsonld = { + '@id': item_url, + '@type': 'http://schema.org/Place', + 'http://schema.org/name': @content.name +} +%> +<%= content_jsonld.to_json.html_safe %> +</script> + <%= render partial: 'content/show', locals: { content: @content } %> diff --git a/app/views/main/dashboard.html.erb b/app/views/main/dashboard.html.erb index 88419bd3..26223218 100644 --- a/app/views/main/dashboard.html.erb +++ b/app/views/main/dashboard.html.erb @@ -211,4 +211,4 @@ </div> </div> -</div> \ No newline at end of file +</div> diff --git a/app/views/main/index.html.erb b/app/views/main/index.html.erb index 5f9d6ece..a9c4af31 100644 --- a/app/views/main/index.html.erb +++ b/app/views/main/index.html.erb @@ -376,4 +376,4 @@ <div class="parallax"><img src="background3.jpg" alt="Unsplashed background img 3" style="display: block; transform: translate3d(-50%, 199px, 0px);"></div> </div> ---> \ No newline at end of file +--> diff --git a/app/views/main/privacyinfo.html.erb b/app/views/main/privacyinfo.html.erb index b818af3a..4febb104 100644 --- a/app/views/main/privacyinfo.html.erb +++ b/app/views/main/privacyinfo.html.erb @@ -1,3 +1,8 @@ +<% +set_meta_tags title: 'Privacy Policy', +description: 'Notebook will always do its best to maintain the security and privacy of your data, in order to ensure that it is you and only you that has access to view, modify, or remove it, unless you explicitly designate otherwise.' +%> + <div class="row"> <div class="col s12 m12 l12"> <div class="card hoverable" style="padding: 30px;"> @@ -26,4 +31,4 @@ <div class="card-comments"></div> </div> </div> -</div> \ No newline at end of file +</div> diff --git a/app/views/universes/show.html.erb b/app/views/universes/show.html.erb index 42b8b9f4..2dcdb0f2 100644 --- a/app/views/universes/show.html.erb +++ b/app/views/universes/show.html.erb @@ -1,3 +1,14 @@ +<script type="application/ld+json"> +<% +content_jsonld = { + '@id': item_url, + '@type': 'http://schema.org/Place', + 'http://schema.org/name': @content.name +} +%> +<%= content_jsonld.to_json.html_safe %> +</script> + <%= render partial: 'content/show', locals: { content: @content } %> <div class="col s12 m12 l4"> @@ -10,4 +21,4 @@ <div class="col s12 m12 l4"> <%= render partial: 'content/cards/in_universe_content_list', locals: { content_type: :item, content_list: @content.items } %> -</div> \ No newline at end of file +</div> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 0e971604..db0a0e34 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -1,3 +1,17 @@ +<script type="application/ld+json"> +<% +set_meta_tags title: @user.name, description: "#{@user.name}’s profile on notebook.ai" + +content_jsonld = { + '@id': user_url, + '@type': 'http://schema.org/Person', + 'http://schema.org/name': @user.name, + 'http://schema.org/description': "#{@user.name}’s profile on notebook.ai" +} +%> +<%= content_jsonld.to_json.html_safe %> %> +</script> + <% tabs = %w(universes characters locations items) diff --git a/config/environments/development.rb b/config/environments/development.rb index 362f7c58..3f340728 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -39,4 +39,6 @@ PlanCharacters::Application.configure do secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] } } + + default_url_options[:host] = 'localhost:3000' end diff --git a/config/environments/production.rb b/config/environments/production.rb index 889f2eec..9afa9692 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -91,4 +91,6 @@ Rails.application.configure do # Do not dump schema after migrations. # TODO: double check this config.active_record.dump_schema_after_migration = false + + default_url_options[:host] = 'www.notebook.ai' end diff --git a/config/environments/test.rb b/config/environments/test.rb index da1933d6..c228c40d 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -36,4 +36,6 @@ Rails.application.configure do config.active_support.test_order = :random config.active_record.raise_in_transactional_callbacks = true + + default_url_options[:host] = 'localhost:3000' end diff --git a/test/integration/universe_stories_test.rb b/test/integration/universe_stories_test.rb new file mode 100644 index 00000000..ab3704e6 --- /dev/null +++ b/test/integration/universe_stories_test.rb @@ -0,0 +1,43 @@ +require 'test_helper' + +# Tests scenarios related to interacting with Universes +class UniverseStoriesTest < ActionDispatch::IntegrationTest + setup do + @user = log_in_as_user + @universe = create(:universe, user: @user) + end + + test 'universe is displayed on universes list' do + visit universes_path + assert page.has_content?(@universe.name), + "Page body didn't contain universe name: "\ + "#{@universe.name} not found in \n#{page.body}" + end + + test 'universe list edit button edits universe' do + visit universe_path(@universe) + click_on 'Edit this universe' + assert_equal edit_universe_path(@universe), current_path + end + + test 'universe list view button shows universe' do + visit universes_path + within(:css, '.collection-item:first') do + click_on @universe.name + end + assert_equal universe_path(@universe), current_path, + "Not on universe path for universe #{@universe.name}: "\ + "#{@universe.name} not found in \n#{page.body}" + end + + test 'a user can create a new universe' do + new_universe = build(:universe) + visit universes_path + click_on 'Create another universe' + fill_in 'universe_name', with: new_universe.name + click_on 'Create Universe' + + assert_equal universe_path(Universe.where(name: new_universe.name).first), + current_path + end +end