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.
This commit is contained in:
parent
18f751938f
commit
a0516f731c
3
Gemfile
3
Gemfile
@ -47,6 +47,9 @@ gem "friendly_id", "~> 5.5"
|
|||||||
|
|
||||||
gem "kaminari", "~> 1.2"
|
gem "kaminari", "~> 1.2"
|
||||||
|
|
||||||
|
gem "meta-tags", "~> 2.22"
|
||||||
|
gem 'sitemap_generator', '~> 6.3'
|
||||||
|
|
||||||
# gem "whenever", "~> 1.0"
|
# gem "whenever", "~> 1.0"
|
||||||
gem "ruby-openai", "~> 7.3"
|
gem "ruby-openai", "~> 7.3"
|
||||||
gem "httparty", "~> 0.22.0"
|
gem "httparty", "~> 0.22.0"
|
||||||
|
@ -233,6 +233,8 @@ GEM
|
|||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.4)
|
marcel (1.0.4)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
|
meta-tags (2.22.1)
|
||||||
|
actionpack (>= 6.0.0, < 8.1)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
minitest (5.25.4)
|
minitest (5.25.4)
|
||||||
msgpack (1.7.5)
|
msgpack (1.7.5)
|
||||||
@ -400,6 +402,8 @@ GEM
|
|||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 6, < 8)
|
sidekiq (>= 6, < 8)
|
||||||
tilt (>= 1.4.0, < 3)
|
tilt (>= 1.4.0, < 3)
|
||||||
|
sitemap_generator (6.3.0)
|
||||||
|
builder (~> 3.0)
|
||||||
solid_cable (3.0.5)
|
solid_cable (3.0.5)
|
||||||
actioncable (>= 7.2)
|
actioncable (>= 7.2)
|
||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
@ -495,6 +499,7 @@ DEPENDENCIES
|
|||||||
jsbundling-rails
|
jsbundling-rails
|
||||||
kamal
|
kamal
|
||||||
kaminari (~> 1.2)
|
kaminari (~> 1.2)
|
||||||
|
meta-tags (~> 2.22)
|
||||||
pg (~> 1.5)
|
pg (~> 1.5)
|
||||||
propshaft
|
propshaft
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
@ -504,6 +509,7 @@ DEPENDENCIES
|
|||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
sidekiq (~> 7.3)
|
sidekiq (~> 7.3)
|
||||||
sidekiq-scheduler (~> 5.0)
|
sidekiq-scheduler (~> 5.0)
|
||||||
|
sitemap_generator (~> 6.3)
|
||||||
solid_cable
|
solid_cable
|
||||||
solid_cache
|
solid_cache
|
||||||
solid_queue
|
solid_queue
|
||||||
|
24
app/concerns/seo_concern.rb
Normal file
24
app/concerns/seo_concern.rb
Normal file
@ -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
|
@ -1,4 +1,5 @@
|
|||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
|
include SeoConcern
|
||||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||||
allow_browser versions: :modern
|
allow_browser versions: :modern
|
||||||
before_action :set_locale
|
before_action :set_locale
|
||||||
|
@ -14,9 +14,24 @@ class CitiesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
@cities = @cities.page(params[:page]).per(10)
|
@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
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@city = City.friendly.find(params[:id])
|
@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
|
||||||
end
|
end
|
||||||
|
@ -2,5 +2,10 @@ class HomeController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6)
|
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6)
|
||||||
@featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5)
|
@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
|
||||||
end
|
end
|
||||||
|
@ -2,5 +2,13 @@ class WeatherArtsController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
@city = City.friendly.find(params[:city_id])
|
@city = City.friendly.find(params[:city_id])
|
||||||
@weather_art = @city.weather_arts.friendly.find(params[:slug])
|
@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
|
||||||
end
|
end
|
||||||
|
@ -1,2 +1,24 @@
|
|||||||
module ApplicationHelper
|
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
|
end
|
||||||
|
@ -5,6 +5,19 @@
|
|||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<%= 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 %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
<% content_for :head do %>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
<%= weather_art_schema(@weather_art) %>
|
||||||
|
</script>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="min-h-screen">
|
<div class="min-h-screen">
|
||||||
<!-- 返回导航 -->
|
<!-- 返回导航 -->
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
@ -3,7 +3,6 @@ class GenerateWeatherArtWorker
|
|||||||
|
|
||||||
def perform(*args)
|
def perform(*args)
|
||||||
city_id = args[0]
|
city_id = args[0]
|
||||||
city = City.find(city_id)
|
|
||||||
return if city.last_weather_fetch&.today?
|
return if city.last_weather_fetch&.today?
|
||||||
|
|
||||||
weather_service = WeatherService.new
|
weather_service = WeatherService.new
|
||||||
|
35
app/workers/refresh_sitemap_worker.rb
Normal file
35
app/workers/refresh_sitemap_worker.rb
Normal file
@ -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
|
52
config/initializers/meta_tags.rb
Normal file
52
config/initializers/meta_tags.rb
Normal file
@ -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 <title> 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
|
@ -3,3 +3,10 @@ batch_generate_weather:
|
|||||||
class: BatchGenerateWeatherArtsWorker
|
class: BatchGenerateWeatherArtsWorker
|
||||||
description: "Generate weather arts every 2 hours"
|
description: "Generate weather arts every 2 hours"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
refresh_sitemap:
|
||||||
|
cron: '0 5 * * *'
|
||||||
|
class: RefreshSitemapWorker
|
||||||
|
queue: default
|
||||||
|
description: "Refresh sitemap daily"
|
||||||
|
enabled: true
|
||||||
|
48
config/sitemap.rb
Normal file
48
config/sitemap.rb
Normal file
@ -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
|
@ -1 +1,5 @@
|
|||||||
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
# 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
|
||||||
|
Loading…
Reference in New Issue
Block a user