Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
run: bundle env
- name: Run rake rubocop
run: bundle exec rake rubocop
- name: Validate products.yml
run: bundle exec rake test:products_data
- name: Validate code
run: bundle exec jekyll build

Expand Down
32 changes: 32 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,36 @@ namespace :test do
checks: ['Links'],
).run
end

desc 'Validate _data/products.yml: `latest` must name a real version, and ' \
'`versions` must be ordered newest-first (both are relied on by ' \
'_includes/version-banner.html and the version selector)'
task :products_data do
products = YAML.load_file('_data/products.yml')
errors = []

products.each do |product_id, product|
next if product['single_version']

ids = (product['versions'] || []).map { |v| v['id'] }

errors << "#{product_id}: duplicate version ids (#{ids.join(', ')})" if ids.uniq.length != ids.length

errors << "#{product_id}: latest '#{product['latest']}' is not one of its versions (#{ids.join(', ')})" unless ids.include?(product['latest'])

majors = ids.map { |id| id[/\A\d+/] }
if majors.any?(&:nil?)
errors << "#{product_id}: version id(s) don't start with a number (#{ids.join(', ')}), can't check ordering"
elsif majors.map(&:to_i) != majors.map(&:to_i).sort.reverse
errors << "#{product_id}: versions must be newest-first by id (got #{ids.join(', ')})"
end
end

if errors.any?
warn "_data/products.yml failed validation:\n - #{errors.join("\n - ")}"
exit 1
end

puts '_data/products.yml: latest references and version ordering OK'
end
end
12 changes: 12 additions & 0 deletions _includes/jekyll_vitepress/layout_end.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{% include version-banner.html %}
<script>
(function () {
function syncSidebarNavGroups() {
Expand Down Expand Up @@ -37,10 +38,21 @@
});
});
}
// Show the outdated-version banner matching the current collection, if any.
// Banners exist only for frozen (non-latest) versions; see version-banner.html.
function syncOutdatedBanner() {
var frameState = document.getElementById('vp-page-state');
if (!frameState) return;
var currentCollection = frameState.getAttribute('data-collection') || '';
document.querySelectorAll('.VPOutdatedBanner').forEach(function (banner) {
banner.hidden = banner.getAttribute('data-collection') !== currentCollection;
});
}
document.addEventListener('turbo:frame-load', function (event) {
if (!event.target || event.target.id !== 'vp-content-frame') return;
syncSidebarNavGroups();
syncVersionPicker();
syncOutdatedBanner();
});
})();

Expand Down
48 changes: 48 additions & 0 deletions _includes/version-banner.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{%- comment -%}
Viewport-pinned notice shown on every page of a frozen (non-latest) doc
version, so readers landing on old bookmarks or search results always see
a way to the current docs (#320).

One banner is emitted per numbered version that is OLDER than what the
product's `latest` alias points at, so coverage is automatic: the moment
_data/products.yml gains a new version and `latest` moves, every page of
the older collections shows the banner with no per-page front matter.
Today every numbered version is its product's latest, so this include
renders nothing.

"Older" is positional, not just "not latest": `versions` is documented in
_data/products.yml as newest-first, and a Phase 1 major-version cutover
(see MAINTAINING.md) adds a new, newer version at the front of the list
while `latest` deliberately stays on the prior version until GA (e.g.
OpenVox 9.x sitting ahead of the 8x `latest` entry during its beta). A
version listed ahead of `latest` is a preview that hasn't been promoted
yet, not something "no longer actively updated" — it must not get this
banner. So the loop below tracks whether `latest` has been passed yet and
only emits banners after that point.

Like the version picker, the banners live outside the Turbo content frame
and persist across navigation: Liquid sets the initial visibility from
page.collection, and the layout-end script re-syncs on every frame load.
The link targets the product's /latest/ root rather than the same page
under /latest/ — there is no redirect mechanism, so a same-page link 404s
whenever the page was renamed or removed in the newer version. Revisit
once the 404 page can search for the missing page.
{%- endcomment -%}
{%- for product_entry in site.data.products -%}
{%- assign product_id = product_entry[0] -%}
{%- assign product = product_entry[1] -%}
{%- if product.single_version == true -%}{%- continue -%}{%- endif -%}
{%- assign passed_latest = false -%}
{%- for version in product.versions -%}
{%- if version.id == product.latest -%}
{%- assign passed_latest = true -%}
{%- continue -%}
{%- endif -%}
{%- unless passed_latest -%}{%- continue -%}{%- endunless -%}
{%- assign version_collection = version.collection | remove_first: '_' -%}
<div class="VPOutdatedBanner" role="region" aria-label="Outdated documentation notice" data-collection="{{ version_collection }}"{% if version_collection != page.collection %} hidden{% endif %}>
This documentation is for {{ product.label }} {{ version.label }}, which is no longer actively updated.
<a href="/{{ product_id }}/latest/">Switch to the latest {{ product.label }} documentation</a>.
</div>
{%- endfor -%}
{%- endfor -%}
43 changes: 43 additions & 0 deletions _plugins/canonical_latest.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

# Every numbered collection whose version *is* its product's `latest` alias
# (e.g. _openvox_8x while `latest: 8x`) publishes byte-identical content twice:
# once under its own base (/openvox/8.x/) and once under /openvox/latest/ via
# the _latest symlink. This hook points the numbered copy's canonical at its
# /latest/ twin so search engines index the stable /latest/ URLs; the theme's
# head.html (and jekyll-seo-tag, if adopted) emit page.canonical_url when set.
#
# Frozen older versions are deliberately left alone with the theme's default
# self-canonical: once `latest` moves on, their content is unique and the
# same page may not exist under /latest/.
#
# The origin is hardcoded, matching llms.txt, because site.url is unset.
module OpenvoxDocs
module CanonicalLatest
ORIGIN = 'https://docs.openvoxproject.org'

def self.apply(site)
(site.data['products'] || {}).each do |product_id, product|
version = Array(product['versions']).find { |v| v['id'] == product['latest'] }
next unless version

collection = site.collections[version['collection'].delete_prefix('_')]
next if collection.nil? || collection.label.end_with?('_latest')

canonicalize(collection, version['base'], "/#{product_id}/latest/")
end
end

def self.canonicalize(collection, base, latest_base)
collection.docs.each do |doc|
next unless doc.url.start_with?(base)

doc.data['canonical_url'] = "#{ORIGIN}#{latest_base}#{doc.url.delete_prefix(base)}"
end
end
end
end

Jekyll::Hooks.register :site, :pre_render do |site|
OpenvoxDocs::CanonicalLatest.apply(site)
end
29 changes: 29 additions & 0 deletions assets/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,35 @@ table.resolution > tbody > tr:last-child > td:first-child {
color: var(--vp-c-white);
}

/* Viewport-pinned notice on pages of frozen (non-latest) doc versions.
The soft danger color is layered over the opaque page background so the
fixed banner doesn't show content scrolling through behind it. */
.VPOutdatedBanner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: var(--vp-z-index-nav);
padding: 0.6rem 1.5rem;
text-align: center;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-danger-1);
background-color: var(--vp-c-bg);
background-image: linear-gradient(var(--vp-c-danger-soft), var(--vp-c-danger-soft));
border-top: 1px solid var(--vp-c-danger-2);
}

.VPOutdatedBanner a {
color: inherit;
font-weight: 600;
text-decoration: underline;
}

.VPOutdatedBanner a:hover {
color: var(--vp-c-danger-2);
}

/* Highlight the active page more visibly */
.VPSidebarItem.level-0.is-active > .item .link > .text,
.VPSidebarItem.level-1.is-active > .item .link > .text,
Expand Down