claude-agent-framework 1.0.0
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.
- package/README.md +128 -0
- package/bin/claude-framework +3 -0
- package/framework/agents/design-lead.md +240 -0
- package/framework/agents/product-owner.md +179 -0
- package/framework/agents/tech-lead.md +226 -0
- package/framework/commands/ayuda.md +127 -0
- package/framework/commands/a/303/261adir.md +98 -0
- package/framework/commands/backup.md +397 -0
- package/framework/commands/cambiar.md +110 -0
- package/framework/commands/cloud.md +457 -0
- package/framework/commands/code.md +142 -0
- package/framework/commands/debug.md +334 -0
- package/framework/commands/deploy.md +383 -0
- package/framework/commands/deshacer.md +120 -0
- package/framework/commands/estado.md +218 -0
- package/framework/commands/explica.md +227 -0
- package/framework/commands/feature.md +120 -0
- package/framework/commands/git.md +427 -0
- package/framework/commands/historial.md +202 -0
- package/framework/commands/learn.md +408 -0
- package/framework/commands/movil.md +245 -0
- package/framework/commands/nuevo.md +118 -0
- package/framework/commands/plan.md +134 -0
- package/framework/commands/prd.md +113 -0
- package/framework/commands/probar.md +148 -0
- package/framework/commands/revisar.md +208 -0
- package/framework/commands/seeds.md +230 -0
- package/framework/commands/seguridad.md +226 -0
- package/framework/commands/tasks.md +157 -0
- package/framework/skills/architecture/algorithms.md +970 -0
- package/framework/skills/architecture/clean-code.md +1080 -0
- package/framework/skills/architecture/design-patterns.md +1984 -0
- package/framework/skills/architecture/functional-programming.md +972 -0
- package/framework/skills/architecture/solid.md +991 -0
- package/framework/skills/cloud/cloud-aws.md +848 -0
- package/framework/skills/cloud/cloud-azure.md +931 -0
- package/framework/skills/cloud/cloud-gcp.md +848 -0
- package/framework/skills/cloud/message-queues.md +1229 -0
- package/framework/skills/core/accessibility.md +401 -0
- package/framework/skills/core/api.md +474 -0
- package/framework/skills/core/authentication.md +306 -0
- package/framework/skills/core/authorization.md +388 -0
- package/framework/skills/core/background-jobs.md +341 -0
- package/framework/skills/core/caching.md +473 -0
- package/framework/skills/core/code-review.md +341 -0
- package/framework/skills/core/controllers.md +290 -0
- package/framework/skills/core/cua.md +285 -0
- package/framework/skills/core/documentation.md +472 -0
- package/framework/skills/core/file-uploads.md +351 -0
- package/framework/skills/core/hotwire-native.md +296 -0
- package/framework/skills/core/hotwire.md +278 -0
- package/framework/skills/core/i18n.md +334 -0
- package/framework/skills/core/imports-exports.md +750 -0
- package/framework/skills/core/infrastructure.md +337 -0
- package/framework/skills/core/models.md +228 -0
- package/framework/skills/core/notifications.md +672 -0
- package/framework/skills/core/payments.md +581 -0
- package/framework/skills/core/performance.md +361 -0
- package/framework/skills/core/rails-scaffold.md +131 -0
- package/framework/skills/core/search.md +518 -0
- package/framework/skills/core/security.md +565 -0
- package/framework/skills/core/seeds.md +307 -0
- package/framework/skills/core/seo.md +542 -0
- package/framework/skills/core/testing.md +393 -0
- package/framework/skills/core/views.md +260 -0
- package/framework/skills/core/websockets.md +564 -0
- package/framework/skills/data/advanced-sql.md +1204 -0
- package/framework/skills/data/nosql.md +1141 -0
- package/framework/skills/devops/containers-advanced.md +1237 -0
- package/framework/skills/devops/debugging.md +834 -0
- package/framework/skills/devops/git-workflow.md +752 -0
- package/framework/skills/devops/networking.md +932 -0
- package/framework/skills/devops/shell-scripting.md +1132 -0
- package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
- package/framework/sub-agents/cloud-agent.md +677 -0
- package/framework/sub-agents/data.md +504 -0
- package/framework/sub-agents/debugging-agent.md +554 -0
- package/framework/sub-agents/devops.md +483 -0
- package/framework/sub-agents/docs.md +176 -0
- package/framework/sub-agents/frontend-dev.md +349 -0
- package/framework/sub-agents/git-workflow-agent.md +697 -0
- package/framework/sub-agents/integrations.md +630 -0
- package/framework/sub-agents/native-dev.md +434 -0
- package/framework/sub-agents/qa.md +138 -0
- package/framework/sub-agents/rails-dev.md +375 -0
- package/framework/sub-agents/security.md +526 -0
- package/framework/sub-agents/ui.md +437 -0
- package/framework/sub-agents/ux.md +284 -0
- package/framework/templates/api-spec.md +500 -0
- package/framework/templates/component-spec.md +248 -0
- package/framework/templates/feature.json +13 -0
- package/framework/templates/model-spec.md +318 -0
- package/framework/templates/prd-template.md +80 -0
- package/framework/templates/task-plan.md +122 -0
- package/framework/templates/task-user-story.md +52 -0
- package/framework/templates/technical-spec.md +260 -0
- package/framework/templates/user-story.md +95 -0
- package/package.json +42 -0
- package/project-templates/CLAUDE.md +42 -0
- package/project-templates/contexts/architecture.md +25 -0
- package/project-templates/contexts/conventions.md +46 -0
- package/project-templates/contexts/design-system.md +47 -0
- package/project-templates/contexts/requirements.md +38 -0
- package/project-templates/contexts/stack.md +30 -0
- package/project-templates/history/active/models.md +11 -0
- package/project-templates/history/changelog.md +15 -0
- package/project-templates/workspace/.gitkeep +0 -0
- package/src/cli.js +52 -0
- package/src/init.js +104 -0
- package/src/status.js +75 -0
- package/src/update.js +88 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
# Skill: SEO
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Optimizar aplicaciones Rails para motores de búsqueda, incluyendo meta tags, structured data, sitemaps, y performance.
|
|
6
|
+
|
|
7
|
+
## Meta Tags
|
|
8
|
+
|
|
9
|
+
### Helper de meta tags
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# app/helpers/meta_tags_helper.rb
|
|
13
|
+
module MetaTagsHelper
|
|
14
|
+
def meta_title(title = nil)
|
|
15
|
+
if title.present?
|
|
16
|
+
content_for(:meta_title) { title }
|
|
17
|
+
else
|
|
18
|
+
content_for?(:meta_title) ? content_for(:meta_title) : t("app.name")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def meta_description(description = nil)
|
|
23
|
+
if description.present?
|
|
24
|
+
content_for(:meta_description) { description }
|
|
25
|
+
else
|
|
26
|
+
content_for?(:meta_description) ? content_for(:meta_description) : t("app.description")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def meta_image(image_url = nil)
|
|
31
|
+
if image_url.present?
|
|
32
|
+
content_for(:meta_image) { image_url }
|
|
33
|
+
else
|
|
34
|
+
content_for?(:meta_image) ? content_for(:meta_image) : asset_url("og-image.png")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def canonical_url(url = nil)
|
|
39
|
+
if url.present?
|
|
40
|
+
content_for(:canonical_url) { url }
|
|
41
|
+
else
|
|
42
|
+
content_for?(:canonical_url) ? content_for(:canonical_url) : request.original_url.split("?").first
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def full_title
|
|
47
|
+
base_title = t("app.name")
|
|
48
|
+
if content_for?(:meta_title)
|
|
49
|
+
"#{content_for(:meta_title)} | #{base_title}"
|
|
50
|
+
else
|
|
51
|
+
base_title
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Layout con meta tags
|
|
58
|
+
|
|
59
|
+
```erb
|
|
60
|
+
<%# app/views/layouts/application.html.erb %>
|
|
61
|
+
<!DOCTYPE html>
|
|
62
|
+
<html lang="<%= I18n.locale %>">
|
|
63
|
+
<head>
|
|
64
|
+
<meta charset="UTF-8">
|
|
65
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
66
|
+
|
|
67
|
+
<%# Título %>
|
|
68
|
+
<title><%= full_title %></title>
|
|
69
|
+
|
|
70
|
+
<%# Meta básicos %>
|
|
71
|
+
<meta name="description" content="<%= meta_description %>">
|
|
72
|
+
<meta name="robots" content="<%= content_for?(:robots) ? content_for(:robots) : 'index, follow' %>">
|
|
73
|
+
|
|
74
|
+
<%# Canonical %>
|
|
75
|
+
<link rel="canonical" href="<%= canonical_url %>">
|
|
76
|
+
|
|
77
|
+
<%# Open Graph %>
|
|
78
|
+
<meta property="og:type" content="<%= content_for?(:og_type) ? content_for(:og_type) : 'website' %>">
|
|
79
|
+
<meta property="og:title" content="<%= meta_title %>">
|
|
80
|
+
<meta property="og:description" content="<%= meta_description %>">
|
|
81
|
+
<meta property="og:image" content="<%= meta_image %>">
|
|
82
|
+
<meta property="og:url" content="<%= canonical_url %>">
|
|
83
|
+
<meta property="og:site_name" content="<%= t('app.name') %>">
|
|
84
|
+
<meta property="og:locale" content="<%= I18n.locale %>">
|
|
85
|
+
|
|
86
|
+
<%# Twitter Card %>
|
|
87
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
88
|
+
<meta name="twitter:title" content="<%= meta_title %>">
|
|
89
|
+
<meta name="twitter:description" content="<%= meta_description %>">
|
|
90
|
+
<meta name="twitter:image" content="<%= meta_image %>">
|
|
91
|
+
<% if Rails.application.credentials.dig(:twitter, :handle) %>
|
|
92
|
+
<meta name="twitter:site" content="@<%= Rails.application.credentials.dig(:twitter, :handle) %>">
|
|
93
|
+
<% end %>
|
|
94
|
+
|
|
95
|
+
<%# Otros %>
|
|
96
|
+
<%= csrf_meta_tags %>
|
|
97
|
+
<%= csp_meta_tag %>
|
|
98
|
+
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
|
99
|
+
<%= javascript_importmap_tags %>
|
|
100
|
+
|
|
101
|
+
<%# Contenido extra en head %>
|
|
102
|
+
<%= yield :head %>
|
|
103
|
+
</head>
|
|
104
|
+
<body>
|
|
105
|
+
<%= yield %>
|
|
106
|
+
</body>
|
|
107
|
+
</html>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Uso en vistas
|
|
111
|
+
|
|
112
|
+
```erb
|
|
113
|
+
<%# app/views/posts/show.html.erb %>
|
|
114
|
+
<%
|
|
115
|
+
meta_title @post.title
|
|
116
|
+
meta_description @post.excerpt || truncate(strip_tags(@post.body), length: 160)
|
|
117
|
+
meta_image url_for(@post.cover_image) if @post.cover_image.attached?
|
|
118
|
+
canonical_url post_url(@post)
|
|
119
|
+
content_for(:og_type) { "article" }
|
|
120
|
+
%>
|
|
121
|
+
|
|
122
|
+
<article>
|
|
123
|
+
<h1><%= @post.title %></h1>
|
|
124
|
+
<%# ... %>
|
|
125
|
+
</article>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Structured Data (Schema.org)
|
|
129
|
+
|
|
130
|
+
### Helper para JSON-LD
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# app/helpers/structured_data_helper.rb
|
|
134
|
+
module StructuredDataHelper
|
|
135
|
+
def json_ld(data)
|
|
136
|
+
content_tag(:script, data.to_json.html_safe, type: "application/ld+json")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def organization_schema
|
|
140
|
+
{
|
|
141
|
+
"@context": "https://schema.org",
|
|
142
|
+
"@type": "Organization",
|
|
143
|
+
name: t("app.name"),
|
|
144
|
+
url: root_url,
|
|
145
|
+
logo: asset_url("logo.png"),
|
|
146
|
+
sameAs: [
|
|
147
|
+
"https://twitter.com/myapp",
|
|
148
|
+
"https://www.linkedin.com/company/myapp"
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def website_schema
|
|
154
|
+
{
|
|
155
|
+
"@context": "https://schema.org",
|
|
156
|
+
"@type": "WebSite",
|
|
157
|
+
name: t("app.name"),
|
|
158
|
+
url: root_url,
|
|
159
|
+
potentialAction: {
|
|
160
|
+
"@type": "SearchAction",
|
|
161
|
+
target: {
|
|
162
|
+
"@type": "EntryPoint",
|
|
163
|
+
urlTemplate: "#{search_url}?q={search_term_string}"
|
|
164
|
+
},
|
|
165
|
+
"query-input": "required name=search_term_string"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def article_schema(post)
|
|
171
|
+
{
|
|
172
|
+
"@context": "https://schema.org",
|
|
173
|
+
"@type": "Article",
|
|
174
|
+
headline: post.title,
|
|
175
|
+
description: post.excerpt,
|
|
176
|
+
image: post.cover_image.attached? ? url_for(post.cover_image) : nil,
|
|
177
|
+
datePublished: post.published_at&.iso8601,
|
|
178
|
+
dateModified: post.updated_at.iso8601,
|
|
179
|
+
author: {
|
|
180
|
+
"@type": "Person",
|
|
181
|
+
name: post.author.name,
|
|
182
|
+
url: user_url(post.author)
|
|
183
|
+
},
|
|
184
|
+
publisher: {
|
|
185
|
+
"@type": "Organization",
|
|
186
|
+
name: t("app.name"),
|
|
187
|
+
logo: {
|
|
188
|
+
"@type": "ImageObject",
|
|
189
|
+
url: asset_url("logo.png")
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
mainEntityOfPage: {
|
|
193
|
+
"@type": "WebPage",
|
|
194
|
+
"@id": post_url(post)
|
|
195
|
+
}
|
|
196
|
+
}.compact
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def breadcrumb_schema(items)
|
|
200
|
+
{
|
|
201
|
+
"@context": "https://schema.org",
|
|
202
|
+
"@type": "BreadcrumbList",
|
|
203
|
+
itemListElement: items.map.with_index(1) do |item, index|
|
|
204
|
+
{
|
|
205
|
+
"@type": "ListItem",
|
|
206
|
+
position: index,
|
|
207
|
+
name: item[:name],
|
|
208
|
+
item: item[:url]
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def product_schema(product)
|
|
215
|
+
{
|
|
216
|
+
"@context": "https://schema.org",
|
|
217
|
+
"@type": "Product",
|
|
218
|
+
name: product.name,
|
|
219
|
+
description: product.description,
|
|
220
|
+
image: product.images.attached? ? product.images.map { |i| url_for(i) } : nil,
|
|
221
|
+
brand: {
|
|
222
|
+
"@type": "Brand",
|
|
223
|
+
name: product.brand
|
|
224
|
+
},
|
|
225
|
+
offers: {
|
|
226
|
+
"@type": "Offer",
|
|
227
|
+
price: product.price,
|
|
228
|
+
priceCurrency: "EUR",
|
|
229
|
+
availability: product.in_stock? ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
|
|
230
|
+
url: product_url(product)
|
|
231
|
+
}
|
|
232
|
+
}.compact
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def faq_schema(faqs)
|
|
236
|
+
{
|
|
237
|
+
"@context": "https://schema.org",
|
|
238
|
+
"@type": "FAQPage",
|
|
239
|
+
mainEntity: faqs.map do |faq|
|
|
240
|
+
{
|
|
241
|
+
"@type": "Question",
|
|
242
|
+
name: faq[:question],
|
|
243
|
+
acceptedAnswer: {
|
|
244
|
+
"@type": "Answer",
|
|
245
|
+
text: faq[:answer]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
end
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Uso en layout
|
|
255
|
+
|
|
256
|
+
```erb
|
|
257
|
+
<%# app/views/layouts/application.html.erb %>
|
|
258
|
+
<head>
|
|
259
|
+
<%# ... otros meta tags ... %>
|
|
260
|
+
|
|
261
|
+
<%# Schema.org global %>
|
|
262
|
+
<%= json_ld(organization_schema) %>
|
|
263
|
+
<%= json_ld(website_schema) %>
|
|
264
|
+
|
|
265
|
+
<%# Schema.org específico de la página %>
|
|
266
|
+
<%= yield :structured_data %>
|
|
267
|
+
</head>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Uso en vistas
|
|
271
|
+
|
|
272
|
+
```erb
|
|
273
|
+
<%# app/views/posts/show.html.erb %>
|
|
274
|
+
<% content_for :structured_data do %>
|
|
275
|
+
<%= json_ld(article_schema(@post)) %>
|
|
276
|
+
<%= json_ld(breadcrumb_schema([
|
|
277
|
+
{ name: "Home", url: root_url },
|
|
278
|
+
{ name: "Blog", url: posts_url },
|
|
279
|
+
{ name: @post.title, url: post_url(@post) }
|
|
280
|
+
])) %>
|
|
281
|
+
<% end %>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Sitemap
|
|
285
|
+
|
|
286
|
+
### Gem sitemap_generator
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
# Gemfile
|
|
290
|
+
gem "sitemap_generator"
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
# config/sitemap.rb
|
|
295
|
+
SitemapGenerator::Sitemap.default_host = "https://myapp.com"
|
|
296
|
+
SitemapGenerator::Sitemap.compress = false # Para debug
|
|
297
|
+
SitemapGenerator::Sitemap.create do
|
|
298
|
+
# Páginas estáticas
|
|
299
|
+
add root_path, changefreq: "daily", priority: 1.0
|
|
300
|
+
add about_path, changefreq: "monthly", priority: 0.7
|
|
301
|
+
add contact_path, changefreq: "monthly", priority: 0.7
|
|
302
|
+
|
|
303
|
+
# Posts
|
|
304
|
+
Post.published.find_each do |post|
|
|
305
|
+
add post_path(post),
|
|
306
|
+
lastmod: post.updated_at,
|
|
307
|
+
changefreq: "weekly",
|
|
308
|
+
priority: 0.8,
|
|
309
|
+
images: post.images.map { |img|
|
|
310
|
+
{ loc: url_for(img), title: post.title }
|
|
311
|
+
}
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Categorías
|
|
315
|
+
Category.find_each do |category|
|
|
316
|
+
add category_path(category),
|
|
317
|
+
lastmod: category.posts.maximum(:updated_at),
|
|
318
|
+
changefreq: "weekly",
|
|
319
|
+
priority: 0.6
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Usuarios/perfiles públicos
|
|
323
|
+
User.with_public_profile.find_each do |user|
|
|
324
|
+
add user_path(user),
|
|
325
|
+
lastmod: user.updated_at,
|
|
326
|
+
changefreq: "weekly",
|
|
327
|
+
priority: 0.5
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
# Generar sitemap
|
|
334
|
+
rails sitemap:refresh
|
|
335
|
+
|
|
336
|
+
# Notificar a motores de búsqueda
|
|
337
|
+
rails sitemap:refresh:no_ping # Sin notificar
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Tarea programada
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
# config/schedule.rb (whenever gem) o Solid Queue
|
|
344
|
+
every 1.day, at: "4:00 am" do
|
|
345
|
+
rake "sitemap:refresh"
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Robots.txt
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
# config/routes.rb
|
|
353
|
+
get "robots.txt", to: "pages#robots", defaults: { format: :text }
|
|
354
|
+
|
|
355
|
+
# app/controllers/pages_controller.rb
|
|
356
|
+
class PagesController < ApplicationController
|
|
357
|
+
def robots
|
|
358
|
+
render plain: <<~ROBOTS
|
|
359
|
+
User-agent: *
|
|
360
|
+
Disallow: /admin/
|
|
361
|
+
Disallow: /api/
|
|
362
|
+
Disallow: /users/sign_in
|
|
363
|
+
Disallow: /users/sign_up
|
|
364
|
+
Disallow: /search?
|
|
365
|
+
|
|
366
|
+
Sitemap: #{sitemap_url}
|
|
367
|
+
ROBOTS
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
private
|
|
371
|
+
|
|
372
|
+
def sitemap_url
|
|
373
|
+
"#{request.protocol}#{request.host_with_port}/sitemap.xml"
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## URLs amigables
|
|
379
|
+
|
|
380
|
+
### Slugs con friendly_id
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
# Gemfile
|
|
384
|
+
gem "friendly_id"
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
# db/migrate/xxx_add_slug_to_posts.rb
|
|
389
|
+
class AddSlugToPosts < ActiveRecord::Migration[8.0]
|
|
390
|
+
def change
|
|
391
|
+
add_column :posts, :slug, :string
|
|
392
|
+
add_index :posts, :slug, unique: true
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# app/models/post.rb
|
|
397
|
+
class Post < ApplicationRecord
|
|
398
|
+
extend FriendlyId
|
|
399
|
+
friendly_id :title, use: [:slugged, :history]
|
|
400
|
+
|
|
401
|
+
# Regenerar slug solo si título cambia significativamente
|
|
402
|
+
def should_generate_new_friendly_id?
|
|
403
|
+
title_changed? || slug.blank?
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# app/controllers/posts_controller.rb
|
|
408
|
+
class PostsController < ApplicationController
|
|
409
|
+
def show
|
|
410
|
+
@post = Post.friendly.find(params[:id])
|
|
411
|
+
|
|
412
|
+
# Redirect a URL canónica si slug cambió
|
|
413
|
+
if request.path != post_path(@post)
|
|
414
|
+
redirect_to @post, status: :moved_permanently
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### URLs localizadas
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
# config/routes.rb
|
|
424
|
+
scope "(:locale)", locale: /es|en/ do
|
|
425
|
+
resources :posts, path_names: {
|
|
426
|
+
new: I18n.t("routes.new"),
|
|
427
|
+
edit: I18n.t("routes.edit")
|
|
428
|
+
}
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# app/controllers/application_controller.rb
|
|
432
|
+
class ApplicationController < ActionController::Base
|
|
433
|
+
around_action :switch_locale
|
|
434
|
+
|
|
435
|
+
def default_url_options
|
|
436
|
+
{ locale: I18n.locale }
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
private
|
|
440
|
+
|
|
441
|
+
def switch_locale(&action)
|
|
442
|
+
locale = params[:locale] || I18n.default_locale
|
|
443
|
+
I18n.with_locale(locale, &action)
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Performance SEO
|
|
449
|
+
|
|
450
|
+
### Lazy loading de imágenes
|
|
451
|
+
|
|
452
|
+
```erb
|
|
453
|
+
<%= image_tag post.cover_image,
|
|
454
|
+
loading: "lazy",
|
|
455
|
+
decoding: "async",
|
|
456
|
+
alt: post.title,
|
|
457
|
+
class: "w-full h-auto" %>
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Preload de recursos críticos
|
|
461
|
+
|
|
462
|
+
```erb
|
|
463
|
+
<head>
|
|
464
|
+
<%# Preload fuentes %>
|
|
465
|
+
<link rel="preload" href="<%= asset_path('Inter.woff2') %>" as="font" type="font/woff2" crossorigin>
|
|
466
|
+
|
|
467
|
+
<%# Preload imagen LCP %>
|
|
468
|
+
<% if content_for?(:preload_image) %>
|
|
469
|
+
<link rel="preload" href="<%= content_for(:preload_image) %>" as="image">
|
|
470
|
+
<% end %>
|
|
471
|
+
|
|
472
|
+
<%# Preconnect a servicios externos %>
|
|
473
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
474
|
+
<link rel="preconnect" href="https://www.google-analytics.com">
|
|
475
|
+
</head>
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Core Web Vitals
|
|
479
|
+
|
|
480
|
+
```erb
|
|
481
|
+
<%# Evitar CLS (Cumulative Layout Shift) %>
|
|
482
|
+
<%= image_tag post.image,
|
|
483
|
+
width: 800,
|
|
484
|
+
height: 600,
|
|
485
|
+
class: "aspect-[4/3] object-cover" %>
|
|
486
|
+
|
|
487
|
+
<%# Skeleton para contenido dinámico %>
|
|
488
|
+
<div class="min-h-[200px]"> <!-- Reservar espacio -->
|
|
489
|
+
<%= turbo_frame_tag "comments", src: post_comments_path(@post) do %>
|
|
490
|
+
<div class="animate-pulse bg-gray-200 h-32 rounded"></div>
|
|
491
|
+
<% end %>
|
|
492
|
+
</div>
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Hreflang para múltiples idiomas
|
|
496
|
+
|
|
497
|
+
```erb
|
|
498
|
+
<head>
|
|
499
|
+
<% I18n.available_locales.each do |locale| %>
|
|
500
|
+
<link rel="alternate"
|
|
501
|
+
hreflang="<%= locale %>"
|
|
502
|
+
href="<%= url_for(locale: locale) %>">
|
|
503
|
+
<% end %>
|
|
504
|
+
<link rel="alternate" hreflang="x-default" href="<%= url_for(locale: I18n.default_locale) %>">
|
|
505
|
+
</head>
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## Checklist SEO
|
|
509
|
+
|
|
510
|
+
### Técnico
|
|
511
|
+
- [ ] Meta title único por página (50-60 chars)
|
|
512
|
+
- [ ] Meta description única (150-160 chars)
|
|
513
|
+
- [ ] URLs amigables con slugs
|
|
514
|
+
- [ ] Canonical URLs configuradas
|
|
515
|
+
- [ ] Sitemap.xml generado y actualizado
|
|
516
|
+
- [ ] Robots.txt configurado
|
|
517
|
+
- [ ] HTTPS habilitado
|
|
518
|
+
- [ ] Mobile-friendly (responsive)
|
|
519
|
+
|
|
520
|
+
### Contenido
|
|
521
|
+
- [ ] H1 único por página
|
|
522
|
+
- [ ] Jerarquía de headings (H1 > H2 > H3)
|
|
523
|
+
- [ ] Alt text en imágenes
|
|
524
|
+
- [ ] Internal linking
|
|
525
|
+
|
|
526
|
+
### Structured Data
|
|
527
|
+
- [ ] Organization schema
|
|
528
|
+
- [ ] Article schema para posts
|
|
529
|
+
- [ ] BreadcrumbList schema
|
|
530
|
+
- [ ] Product schema (si aplica)
|
|
531
|
+
- [ ] FAQ schema (si aplica)
|
|
532
|
+
|
|
533
|
+
### Social
|
|
534
|
+
- [ ] Open Graph tags
|
|
535
|
+
- [ ] Twitter Card tags
|
|
536
|
+
- [ ] Imagen OG (1200x630px)
|
|
537
|
+
|
|
538
|
+
### Performance
|
|
539
|
+
- [ ] Core Web Vitals optimizados
|
|
540
|
+
- [ ] Imágenes optimizadas (WebP, lazy loading)
|
|
541
|
+
- [ ] CSS/JS minificados
|
|
542
|
+
- [ ] Preload de recursos críticos
|