From a0516f731ca68fa4749f78014b1398627ed2f8cb Mon Sep 17 00:00:00 2001 From: songtianlun Date: Thu, 23 Jan 2025 19:02:52 +0800 Subject: [PATCH] feat: add SEO meta tags and sitemap generation - Introduced `SeoConcern` module to handle SEO meta tags - Integrated `meta-tags` gem for customizable meta tags - Created `RefreshSitemapWorker` to automate sitemap updates - Added relevant meta tags in controllers for weather art and cities - Configured sitemap generation settings These changes improve the SEO of the application by ensuring that pages have appropriate meta tags. Additionally, a sitemap is now generated and refreshed daily, enhancing site visibility to search engines. --- Gemfile | 3 ++ Gemfile.lock | 6 +++ app/concerns/seo_concern.rb | 24 ++++++++++ app/controllers/application_controller.rb | 1 + app/controllers/cities_controller.rb | 15 +++++++ app/controllers/home_controller.rb | 5 +++ app/controllers/weather_arts_controller.rb | 8 ++++ app/helpers/application_helper.rb | 22 +++++++++ app/views/layouts/application.html.erb | 13 ++++++ app/views/weather_arts/show.html.erb | 6 +++ app/workers/generate_weather_art_worker.rb | 1 - app/workers/refresh_sitemap_worker.rb | 35 +++++++++++++++ config/initializers/meta_tags.rb | 52 ++++++++++++++++++++++ config/sidekiq.yml | 7 +++ config/sitemap.rb | 48 ++++++++++++++++++++ public/robots.txt | 4 ++ 16 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 app/concerns/seo_concern.rb create mode 100644 app/workers/refresh_sitemap_worker.rb create mode 100644 config/initializers/meta_tags.rb create mode 100644 config/sitemap.rb diff --git a/Gemfile b/Gemfile index 18c9b8a..76925c1 100644 --- a/Gemfile +++ b/Gemfile @@ -47,6 +47,9 @@ gem "friendly_id", "~> 5.5" gem "kaminari", "~> 1.2" +gem "meta-tags", "~> 2.22" +gem 'sitemap_generator', '~> 6.3' + # gem "whenever", "~> 1.0" gem "ruby-openai", "~> 7.3" gem "httparty", "~> 0.22.0" diff --git a/Gemfile.lock b/Gemfile.lock index 52401fe..a22c78e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -233,6 +233,8 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + meta-tags (2.22.1) + actionpack (>= 6.0.0, < 8.1) mini_mime (1.1.5) minitest (5.25.4) msgpack (1.7.5) @@ -400,6 +402,8 @@ GEM rufus-scheduler (~> 3.2) sidekiq (>= 6, < 8) tilt (>= 1.4.0, < 3) + sitemap_generator (6.3.0) + builder (~> 3.0) solid_cable (3.0.5) actioncable (>= 7.2) activejob (>= 7.2) @@ -495,6 +499,7 @@ DEPENDENCIES jsbundling-rails kamal kaminari (~> 1.2) + meta-tags (~> 2.22) pg (~> 1.5) propshaft puma (>= 5.0) @@ -504,6 +509,7 @@ DEPENDENCIES selenium-webdriver sidekiq (~> 7.3) sidekiq-scheduler (~> 5.0) + sitemap_generator (~> 6.3) solid_cable solid_cache solid_queue diff --git a/app/concerns/seo_concern.rb b/app/concerns/seo_concern.rb new file mode 100644 index 0000000..b428ea4 --- /dev/null +++ b/app/concerns/seo_concern.rb @@ -0,0 +1,24 @@ +# app/concerns/seo_concern.rb +module SeoConcern + extend ActiveSupport::Concern + + included do + before_action :prepare_meta_tags + end + + private + + def prepare_meta_tags + set_meta_tags( + site: "TodayAIWeather", + description: "Discover AI-generated weather art from cities around the world. Real-time weather visualization through artificial intelligence.", + keywords: "weather, AI art, weather visualization, city weather, artificial intelligence", + og: { + title: :title, + description: :description, + type: "website", + url: request.original_url, + } + ) + end +end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1ae736c..2c24e4d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,5 @@ class ApplicationController < ActionController::Base + include SeoConcern # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. allow_browser versions: :modern before_action :set_locale diff --git a/app/controllers/cities_controller.rb b/app/controllers/cities_controller.rb index 504c0f5..a310b9d 100644 --- a/app/controllers/cities_controller.rb +++ b/app/controllers/cities_controller.rb @@ -14,9 +14,24 @@ class CitiesController < ApplicationController end @cities = @cities.page(params[:page]).per(10) + + set_meta_tags( + title: @current_region ? "Cities in #{@current_region.name}" : "Explore Cities", + description: "Discover weather art for cities #{@current_region ? "in #{@current_region.name}" : 'worldwide'}. Real-time AI-generated weather visualization.", + keywords: "#{@current_region&.name}, cities, weather art, AI visualization" + ) end def show @city = City.friendly.find(params[:id]) + + set_meta_tags( + title: @city.name, + description: "Experience #{@city.name}'s weather through AI-generated art. Daily updates of weather conditions visualized through artificial intelligence.", + keywords: "#{@city.name}, #{@city.country.name}, weather art, AI visualization", + og: { + image: @city.latest_weather_art&.image&.attached? ? url_for(@city.latest_weather_art.image) : nil + } + ) end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 14cef42..8329ff9 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,5 +2,10 @@ class HomeController < ApplicationController def index @latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6) @featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5) + set_meta_tags( + title: "AI-Generated Weather Art", + description: "Experience weather through artistic AI visualization. Daily updated weather art for cities worldwide.", + keywords: "AI weather art, weather visualization, city weather, artificial intelligence" + ) end end diff --git a/app/controllers/weather_arts_controller.rb b/app/controllers/weather_arts_controller.rb index 5ae79ec..0cf9a2e 100644 --- a/app/controllers/weather_arts_controller.rb +++ b/app/controllers/weather_arts_controller.rb @@ -2,5 +2,13 @@ class WeatherArtsController < ApplicationController def show @city = City.friendly.find(params[:city_id]) @weather_art = @city.weather_arts.friendly.find(params[:slug]) + set_meta_tags( + title: "#{@city.name} Weather Art - #{@weather_art.weather_date.strftime('%B %d, %Y')}", + description: "#{@city.name}'s weather visualized through AI art. #{@weather_art.description} at #{@weather_art.temperature}°C.", + keywords: "#{@city.name}, weather art, #{@weather_art.description}, AI visualization", + og: { + image: @weather_art.image.attached? ? url_for(@weather_art.image) : nil + } + ) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be79..2b5c010 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,24 @@ module ApplicationHelper + def weather_art_schema(weather_art) + { + "@context": "https://schema.org", + "@type": "ImageObject", + "name": "#{weather_art.city.name} Weather Art", + "description": weather_art.description, + "datePublished": weather_art.created_at.iso8601, + "contentUrl": url_for(weather_art.image), + "author": { + "@type": "Organization", + "name": "TodayAIWeather" + }, + "locationCreated": { + "@type": "Place", + "name": weather_art.city.name, + "address": { + "@type": "PostalAddress", + "addressCountry": weather_art.city.country.name + } + } + }.to_json.html_safe if weather_art.image.attached? + end end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 80446f1..77a0a81 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -5,6 +5,19 @@ + <%= display_meta_tags( + site: 'TodayAIWeather', + reverse: true, + og: { + site_name: 'TodayAIWeather', + type: 'website', + url: request.original_url + }, + alternate: { + "zh-CN" => url_for(locale: 'zh-CN'), + "en" => url_for(locale: 'en') + } + ) %> <%= csrf_meta_tags %> <%= csp_meta_tag %> diff --git a/app/views/weather_arts/show.html.erb b/app/views/weather_arts/show.html.erb index 3d9d3d9..58d67bb 100644 --- a/app/views/weather_arts/show.html.erb +++ b/app/views/weather_arts/show.html.erb @@ -1,3 +1,9 @@ +<% content_for :head do %> + +<% end %> +
diff --git a/app/workers/generate_weather_art_worker.rb b/app/workers/generate_weather_art_worker.rb index b2deb18..6d0c622 100644 --- a/app/workers/generate_weather_art_worker.rb +++ b/app/workers/generate_weather_art_worker.rb @@ -3,7 +3,6 @@ class GenerateWeatherArtWorker def perform(*args) city_id = args[0] - city = City.find(city_id) return if city.last_weather_fetch&.today? weather_service = WeatherService.new diff --git a/app/workers/refresh_sitemap_worker.rb b/app/workers/refresh_sitemap_worker.rb new file mode 100644 index 0000000..77d51b8 --- /dev/null +++ b/app/workers/refresh_sitemap_worker.rb @@ -0,0 +1,35 @@ +class RefreshSitemapWorker + include Sidekiq::Worker + + def perform + SitemapGenerator::Sitemap.create do + add root_path, changefreq: "daily", priority: 1.0 + add cities_path, changefreq: "daily", priority: 0.9 + add arts_path, changefreq: "daily", priority: 0.9 + + City.find_each do |city| + add city_path(city), + changefreq: "daily", + priority: 0.8, + lastmod: city.updated_at + end + + WeatherArt.includes(:city).find_each do |art| + if art.image.attached? + add city_weather_art_path(art.city, art), + changefreq: "daily", + priority: 0.7, + lastmod: art.updated_at, + images: [{ + loc: url_for(art.image), + title: "#{art.city.name} Weather Art - #{art.weather_date.strftime('%B %d, %Y')}" + }] + end + end + end + + SitemapGenerator::Sitemap.ping_search_engines if Rails.env.production? + rescue => e + Rails.logger.error "Error refreshing sitemap: #{e.message}" + end +end \ No newline at end of file diff --git a/config/initializers/meta_tags.rb b/config/initializers/meta_tags.rb new file mode 100644 index 0000000..8dcd7a9 --- /dev/null +++ b/config/initializers/meta_tags.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Use this setup block to configure all options available in MetaTags. +MetaTags.configure do |config| + config.title_limit = 70 + config.description_limit = 160 + config.keywords_limit = 255 + # How many characters should the title meta tag have at most. Default is 70. + # Set to nil or 0 to remove limits. + # config.title_limit = 70 + + # When true, site title will be truncated instead of title. Default is false. + # config.truncate_site_title_first = false + + # Add HTML attributes to the HTML tag. Default is {}. + # config.title_tag_attributes = {} + + # Natural separator when truncating. Default is " " (space character). + # Set to nil to disable natural separator. + # This also allows you to use a whitespace regular expression (/\s/) or + # a Unicode space (/\p{Space}/). + # config.truncate_on_natural_separator = " " + + # Maximum length of the page description. Default is 300. + # Set to nil or 0 to remove limits. + # config.description_limit = 300 + + # Maximum length of the keywords meta tag. Default is 255. + # config.keywords_limit = 255 + + # Default separator for keywords meta tag (used when an Array passed with + # the list of keywords). Default is ", ". + # config.keywords_separator = ', ' + + # When true, keywords will be converted to lowercase, otherwise they will + # appear on the page as is. Default is true. + # config.keywords_lowercase = true + + # When true, the output will not include new line characters between meta tags. + # Default is false. + # config.minify_output = false + + # When false, generated meta tags will be self-closing (<meta ... />) instead + # of open (`<meta ...>`). Default is true. + # config.open_meta_tags = true + + # List of additional meta tags that should use "property" attribute instead + # of "name" attribute in <meta> tags. + # config.property_tags.push( + # 'x-hearthstone:deck', + # ) +end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 45d8af6..e370683 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -3,3 +3,10 @@ batch_generate_weather: class: BatchGenerateWeatherArtsWorker description: "Generate weather arts every 2 hours" enabled: true + +refresh_sitemap: + cron: '0 5 * * *' + class: RefreshSitemapWorker + queue: default + description: "Refresh sitemap daily" + enabled: true diff --git a/config/sitemap.rb b/config/sitemap.rb new file mode 100644 index 0000000..988fe03 --- /dev/null +++ b/config/sitemap.rb @@ -0,0 +1,48 @@ +# Set the host name for URL creation +SitemapGenerator::Sitemap.default_host = "https://todayaiweather.com" + +SitemapGenerator::Sitemap.create do + add root_path, changefreq: 'daily', priority: 1.0 + add cities_path, changefreq: 'daily', priority: 0.9 + add arts_path, changefreq: 'daily', priority: 0.9 + + City.find_each do |city| + add city_path(city), + changefreq: 'daily', + priority: 0.8, + lastmod: city.updated_at + end + + WeatherArt.includes(:city).find_each do |art| + add city_weather_art_path(art.city, art), + changefreq: 'daily', + priority: 0.7, + lastmod: art.updated_at, + images: [{ + loc: url_for(art.image), + title: "#{art.city.name} Weather Art - #{art.weather_date.strftime('%B %d, %Y')}" + }] if art.image.attached? + end + # Put links creation logic here. + # + # The root path '/' and sitemap index file are added automatically for you. + # Links are added to the Sitemap in the order they are specified. + # + # Usage: add(path, options={}) + # (default options are used if you don't specify) + # + # Defaults: :priority => 0.5, :changefreq => 'weekly', + # :lastmod => Time.now, :host => default_host + # + # Examples: + # + # Add '/articles' + # + # add articles_path, :priority => 0.7, :changefreq => 'daily' + # + # Add all articles: + # + # Article.find_each do |article| + # add article_path(article), :lastmod => article.updated_at + # end +end diff --git a/public/robots.txt b/public/robots.txt index c19f78a..e9e0b4c 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1 +1,5 @@ # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +User-agent: * +Allow: / + +Sitemap: https://your-domain.com/sitemap.xml.gz