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 "meta-tags", "~> 2.22"
|
||||
gem 'sitemap_generator', '~> 6.3'
|
||||
|
||||
# gem "whenever", "~> 1.0"
|
||||
gem "ruby-openai", "~> 7.3"
|
||||
gem "httparty", "~> 0.22.0"
|
||||
|
@ -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
|
||||
|
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
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -5,6 +5,19 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="apple-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 %>
|
||||
<%= 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="container mx-auto px-4 py-8">
|
||||
|
@ -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
|
||||
|
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
|
||||
description: "Generate weather arts every 2 hours"
|
||||
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
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://your-domain.com/sitemap.xml.gz
|
||||
|
Loading…
Reference in New Issue
Block a user