Skip to content

Commit cb75295

Browse files
committed
feat(seo): comprehensive technical and on-page SEO optimization
- Refactored 'generate_interviews.rb' to prioritize 'Engagement Hooks' in meta descriptions. - Optimized title tag formats to emphasize guest names and technical topics (Hook: Guest | Context). - Improved 'Generators::Core::Meta.clamp' logic to handle word boundaries and sentence endings gracefully. - Synchronized 'robots.txt' with canonical sitemap domain (just3ws.com). - Verified 'VideoObject' schema generation for interview pages to trigger search rich snippets. - Validated sitemap generation for 990+ archival URLs.
1 parent ab3ea85 commit cb75295

2 files changed

Lines changed: 49 additions & 23 deletions

File tree

_plugins/generate_interviews.rb

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,39 +65,55 @@ def generate(site)
6565
id = interview['id']
6666
next unless id # Skip malformed entries
6767

68+
# Look up corresponding video asset
69+
video_asset = site.data.dig('video_assets', 'items').find { |a| a['id'] == interview['video_asset_id'] } ||
70+
site.data.dig('video_assets', 'items').find { |a| a['id'] == id }
71+
72+
# --- SEO TITLE OPTIMIZATION ---
73+
# Format: Hook/Subject: Guest Name | Context
6874
subject = Generators::Core::Text.normalize_subject(interview['title'])
75+
guest_name = Array(interview['interviewees']).first
76+
77+
title_core = if guest_name
78+
"#{subject}: Interview with #{guest_name}"
79+
else
80+
"#{interview['collection']}#{subject}"
81+
end
82+
6983
context_bits = []
70-
context_bits << interview['collection'] if interview['collection']
7184
context_bits << interview['conference'].to_s.strip if interview['conference'].to_s.strip != ''
7285
context_bits << interview['community'].to_s.strip if interview['community'].to_s.strip != ''
73-
context = context_bits.first(2).join(' · ')
74-
75-
title_core = +"#{interview['collection']}#{subject}"
76-
title_core << " (#{context})" unless context.empty?
77-
title_meta = Generators::Core::Meta.clamp(title_core, 70)
78-
79-
description_parts = []
80-
description_parts << "Archive: #{interview['collection']}"
81-
description_parts << "Featuring: #{Array(interview['interviewees']).join(', ')}" if interview['interviewees']
82-
topic = interview['topic'].to_s.strip
83-
description_parts << "Topic: #{topic}" unless topic.empty?
84-
85-
recorded_date = interview['recorded_date'].to_s.strip
86-
description_parts << "Recorded: #{recorded_date}" unless recorded_date.empty?
86+
context = context_bits.first(1).join(' ') # Keep it slim
87+
88+
title_meta = +"#{title_core}"
89+
title_meta << " | #{context}" unless context.empty?
90+
title_meta = Generators::Core::Meta.clamp(title_meta, 70)
91+
92+
# --- SEO DESCRIPTION OPTIMIZATION ---
93+
# Prioritize the "Engagement Hook" from video_assets.yml
94+
if video_asset && video_asset['description'] && video_asset['description'].length > 50
95+
description_meta = Generators::Core::Meta.clamp(video_asset['description'], 160)
96+
else
97+
description_parts = []
98+
description_parts << "Archive: #{interview['collection']}"
99+
description_parts << "Featuring: #{Array(interview['interviewees']).join(', ')}" if interview['interviewees']
100+
topic = interview['topic'].to_s.strip
101+
description_parts << "Topic: #{topic}" unless topic.empty?
102+
recorded_date = interview['recorded_date'].to_s.strip
103+
description_parts << "Recorded: #{recorded_date}" unless recorded_date.empty?
104+
description_meta = Generators::Core::Meta.clamp("#{description_parts.join('. ')}.", 160)
105+
end
87106

88-
description_parts << "ID: #{id}"
89-
description_meta = Generators::Core::Meta.clamp("#{description_parts.join('. ')}.", 160)
90107
description_meta = Generators::Core::Meta.ensure_min_length(
91108
description_meta,
92109
70,
93110
"Part of Mike Hall's technical video and interview archive."
94111
)
95112
description_meta = Generators::Core::Meta.clamp(description_meta, 160)
113+
96114
title_meta = Generators::Core::Meta.ensure_unique(title_meta, 70, id, seen_titles)
97115
description_meta = Generators::Core::Meta.ensure_unique(description_meta, 160, id, seen_descriptions)
98116

99-
# Look up thumbnail for social preview
100-
video_asset = site.data.dig('video_assets', 'items').find { |a| a['id'] == interview['video_asset_id'] }
101117
thumbnail = video_asset['thumbnail'] if video_asset
102118

103119
site.pages << InterviewPage.new(site, site.source, id, {
@@ -108,7 +124,8 @@ def generate(site)
108124
'breadcrumb_parent_name' => 'Conversations',
109125
'breadcrumb_parent_url' => '/interviews/',
110126
'interview_id' => id,
111-
'collection' => interview['collection']
127+
'collection' => interview['collection'],
128+
'asset' => video_asset # Inject for schema-factory.html
112129
})
113130
end
114131
end

src/generators/core/meta.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,21 @@ def clamp(text, max_length)
88
return clean if max_length <= 0
99
return clean if clean.length <= max_length
1010

11+
# If it fits perfectly after stripping trailing punctuation
12+
trimmed = clean[0, max_length].strip
13+
return trimmed if clean.length <= max_length + 3 && clean =~ /[.!?]$/
14+
15+
# Standard truncation at word boundary
1116
truncated = clean[0, max_length - 1]
1217
return "" if truncated.nil? || truncated.empty?
1318

14-
truncated = truncated.rpartition(" ").first if truncated.include?(" ")
15-
truncated = clean[0, max_length - 1] if truncated.nil? || truncated.empty?
16-
"#{truncated}…"
19+
# Find last word boundary
20+
last_space = truncated.rindex(" ")
21+
if last_space && last_space > (max_length * 0.7)
22+
truncated = truncated[0, last_space]
23+
end
24+
25+
"#{truncated.strip}…"
1726
end
1827

1928
def ensure_unique(value, max_length, disambiguator, seen)

0 commit comments

Comments
 (0)