diff --git a/Gemfile.lock b/Gemfile.lock
index 71c5d4d67..46bb9a01d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -169,7 +169,7 @@ GEM
rest-client (>= 2.0.0)
cocoon (1.2.15)
colorize (0.8.1)
- concurrent-ruby (1.3.5)
+ concurrent-ruby (1.3.4)
countable-rails (0.0.1)
railties (>= 3.1)
crack (0.4.5)
diff --git a/app/assets/stylesheets/osem-splash.scss b/app/assets/stylesheets/osem-splash.scss
index 35a27dca5..e6e31ebd5 100644
--- a/app/assets/stylesheets/osem-splash.scss
+++ b/app/assets/stylesheets/osem-splash.scss
@@ -173,6 +173,26 @@
color: white;
}
}
+
+ #video {
+ .video-wrapper {
+ position: relative;
+ padding-bottom: 56.25%; /* 16:9 aspect ratio */
+ padding-top: 25px;
+ height: 0;
+ max-width: 1080px;
+ margin: 0 auto;
+
+ iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: none;
+ }
+ }
+ }
}
.publicorprivate {
diff --git a/app/controllers/admin/splashpages_controller.rb b/app/controllers/admin/splashpages_controller.rb
index ad4f5e9bc..f42cb0018 100644
--- a/app/controllers/admin/splashpages_controller.rb
+++ b/app/controllers/admin/splashpages_controller.rb
@@ -50,7 +50,8 @@ def splashpage_params
:include_venue, :include_registrations,
:include_tickets, :include_lodgings,
:include_sponsors, :include_social_media,
- :include_booths, :include_past_editions)
+ :include_booths, :include_past_editions,
+ :video_url)
end
end
end
diff --git a/app/models/splashpage.rb b/app/models/splashpage.rb
index 5cd9ef0d3..c2869a7b7 100644
--- a/app/models/splashpage.rb
+++ b/app/models/splashpage.rb
@@ -4,4 +4,109 @@ class Splashpage < ApplicationRecord
belongs_to :conference
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
+
+ validates :video_url,
+ format: { with: URI::regexp(%w(http https)), message: :invalid_url },
+ allow_blank: true
+
+ validate :video_url_is_supported_platform, if: :video_url?
+
+ # Configuration for supported video platforms
+ # To add a new platform: add an entry with patterns (regex) and embed_template
+ SUPPORTED_PLATFORMS = {
+ youtube: {
+ patterns: [
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|m\.youtube\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/
+ ],
+ embed_template: 'https://www.youtube.com/embed/%{video_id}',
+ autoplay_params: 'autoplay=1&loop=1&mute=1&controls=0&disablekb=1&fs=0&iv_load_policy=3&modestbranding=1&playsinline=1'
+ },
+ vimeo: {
+ patterns: [/vimeo\.com\/(?:video\/)?(\d+)/],
+ embed_template: 'https://player.vimeo.com/video/%{video_id}',
+ autoplay_params: 'autoplay=1&muted=1&loop=1&playsinline=1'
+ },
+ peertube: {
+ patterns: [
+ /(https?:\/\/[^\/]+)\/videos\/watch\/([a-zA-Z0-9-]+)/,
+ /(https?:\/\/[^\/]+)\/w\/([a-zA-Z0-9-]+)/
+ ],
+ embed_template: '%{instance_url}/videos/embed/%{video_id}',
+ autoplay_params: 'autoplay=1&loop=1'
+ },
+ invidious: {
+ patterns: [/(https?:\/\/[^\/]+)\/watch\?v=([a-zA-Z0-9_-]{11})/],
+ embed_template: '%{instance_url}/embed/%{video_id}',
+ autoplay_params: 'autoplay=1&loop=1'
+ }
+ }.freeze
+
+ # Detect platform from video URL
+ def video_platform
+ return nil unless video_url.present?
+
+ SUPPORTED_PLATFORMS.find do |platform, config|
+ config[:patterns].any? { |pattern| video_url.match?(pattern) }
+ end&.first
+ end
+
+ # Extract video ID from URL
+ def video_id
+ match_data = url_match_data
+ return nil unless match_data
+
+ # For federated platforms (PeerTube, Invidious), video_id is the second capture
+ match_data.captures.length > 1 ? match_data[2] : match_data[1]
+ end
+
+ # Extract instance URL for federated platforms (PeerTube, Invidious)
+ def video_instance_url
+ return nil unless [:peertube, :invidious].include?(video_platform)
+
+ match_data = url_match_data
+ match_data&.captures&.length.to_i > 1 ? match_data[1] : nil
+ end
+
+ # Generate complete embed URL with autoplay parameters
+ def video_embed_url
+ return nil unless video_platform && video_id
+
+ config = SUPPORTED_PLATFORMS[video_platform]
+ base_url = build_base_url(config)
+
+ "#{base_url}?#{config[:autoplay_params]}"
+ end
+
+ private
+
+ # Get regex match data for the current video URL
+ def url_match_data
+ return nil unless video_url.present? && video_platform.present?
+
+ config = SUPPORTED_PLATFORMS[video_platform]
+ config[:patterns].each do |pattern|
+ match = video_url.match(pattern)
+ return match if match
+ end
+
+ nil
+ end
+
+ # Build base embed URL from template
+ def build_base_url(config)
+ template = config[:embed_template]
+
+ if [:peertube, :invidious].include?(video_platform)
+ template % { instance_url: video_instance_url, video_id: video_id }
+ else
+ template % { video_id: video_id }
+ end
+ end
+
+ # Validation: ensure URL is from a supported platform
+ def video_url_is_supported_platform
+ return if video_platform.present?
+
+ errors.add(:video_url, :unsupported_platform)
+ end
end
diff --git a/app/views/admin/splashpages/_form.html.haml b/app/views/admin/splashpages/_form.html.haml
index 80e128476..d189fd81f 100644
--- a/app/views/admin/splashpages/_form.html.haml
+++ b/app/views/admin/splashpages/_form.html.haml
@@ -45,6 +45,16 @@
%label
= f.check_box :include_social_media
Display the social media links?
+ %h4
+ = t('admin.splashpages.form.video.title')
+ %hr
+ .form-group
+ = f.label :video_url
+ = f.url_field :video_url,
+ class: 'form-control',
+ placeholder: t('admin.splashpages.form.video.placeholder')
+ %p.help-block
+ = t('admin.splashpages.form.video.help_html').html_safe
%h4
Access
%hr
diff --git a/app/views/conferences/_video.haml b/app/views/conferences/_video.haml
new file mode 100644
index 000000000..1e4dfede0
--- /dev/null
+++ b/app/views/conferences/_video.haml
@@ -0,0 +1,14 @@
+- cache [splashpage, splashpage.video_url, '#splash#video'] do
+ - if splashpage.video_embed_url.present?
+ %section#video
+ .container
+ .row
+ .col-md-8.col-md-offset-2
+ .video-wrapper
+ %iframe#video-iframe{
+ src: splashpage.video_embed_url,
+ frameborder: "0",
+ allowfullscreen: "",
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
+ title: "Conference Video"
+ }
diff --git a/app/views/conferences/show.html.haml b/app/views/conferences/show.html.haml
index 3c3278753..5e25e628d 100644
--- a/app/views/conferences/show.html.haml
+++ b/app/views/conferences/show.html.haml
@@ -24,6 +24,9 @@
-# header/description
= render 'header', conference: @conference, venue: @conference.venue
+ -# Video section
+ = render 'video', splashpage: @conference.splashpage
+
-# calls for content, or program
- if @conference.splashpage.include_cfp
= render 'call_for_content', conference: @conference,
diff --git a/config/locales/en.yml b/config/locales/en.yml
index f88786259..41a606429 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -48,3 +48,12 @@ en:
nickname: Nickname
biography: Biography
affiliation: Affiliation
+ splashpage:
+ video_url: Video URL
+ errors:
+ models:
+ splashpage:
+ attributes:
+ video_url:
+ invalid_url: must be a valid URL
+ unsupported_platform: must be from a supported platform (Youtube, Vimeo, Peertube, Invidious)
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 4c13aaaef..44b7d018e 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -13,3 +13,12 @@ es:
nickname: Nickname
biography: Biografía
affiliation: Afiliación
+ splashpage:
+ video_url: URL del vídeo
+ errors:
+ models:
+ splashpage:
+ attributes:
+ video_url:
+ invalid_url: debe ser una URL válida
+ unsupported_platform: debe ser de una plataforma soportada (Youtube, Vimeo, Peertube, Invidious)
diff --git a/config/locales/views/splashpages/en.yml b/config/locales/views/splashpages/en.yml
new file mode 100644
index 000000000..525d22567
--- /dev/null
+++ b/config/locales/views/splashpages/en.yml
@@ -0,0 +1,12 @@
+en:
+ admin:
+ splashpages:
+ form:
+ video:
+ title: Video
+ label: Video URL
+ placeholder: "https://www.youtube.com/watch?v=..."
+ help_html: |
+ Video URL to display on the splash page (will autoplay muted).
+
+ Supported: YouTube, Vimeo, PeerTube, Invidious
diff --git a/config/locales/views/splashpages/es.yml b/config/locales/views/splashpages/es.yml
new file mode 100644
index 000000000..81e22d668
--- /dev/null
+++ b/config/locales/views/splashpages/es.yml
@@ -0,0 +1,12 @@
+es:
+ admin:
+ splashpages:
+ form:
+ video:
+ title: Vídeo
+ label: URL del vídeo
+ placeholder: "https://www.youtube.com/watch?v=..."
+ help_html: |
+ URL del vídeo para mostrar en la página de inicio (se reproducirá automáticamente silenciado).
+
+ Soportados: YouTube, Vimeo, PeerTube, Invidious
diff --git a/db/migrate/20260208190303_add_video_url_to_splashpages.rb b/db/migrate/20260208190303_add_video_url_to_splashpages.rb
new file mode 100644
index 000000000..b69a875dd
--- /dev/null
+++ b/db/migrate/20260208190303_add_video_url_to_splashpages.rb
@@ -0,0 +1,5 @@
+class AddVideoUrlToSplashpages < ActiveRecord::Migration[7.0]
+ def change
+ add_column :splashpages, :video_url, :string
+ end
+end
diff --git a/spec/factories/splashpages.rb b/spec/factories/splashpages.rb
index e52e8478e..ff6af28eb 100644
--- a/spec/factories/splashpages.rb
+++ b/spec/factories/splashpages.rb
@@ -20,6 +20,22 @@
include_sponsors { true }
include_lodgings { true }
include_cfp { true }
+ video_url { 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' }
+ end
+
+ factory :splashpage_with_youtube_video do
+ public { true }
+ video_url { 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' }
+ end
+
+ factory :splashpage_with_vimeo_video do
+ public { true }
+ video_url { 'https://vimeo.com/123456789' }
+ end
+
+ factory :splashpage_with_peertube_video do
+ public { true }
+ video_url { 'https://peertube.example.com/videos/watch/abc-123-def' }
end
end
end
diff --git a/spec/models/splashpage_spec.rb b/spec/models/splashpage_spec.rb
new file mode 100644
index 000000000..c9366cc67
--- /dev/null
+++ b/spec/models/splashpage_spec.rb
@@ -0,0 +1,250 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Splashpage do
+ let(:conference) { create(:conference) }
+ let(:splashpage) { build(:splashpage, conference: conference) }
+
+ describe 'validations' do
+ context 'video_url' do
+ it 'allows blank video_url' do
+ splashpage.video_url = nil
+ expect(splashpage.valid?).to be true
+ end
+
+ it 'allows empty string video_url' do
+ splashpage.video_url = ''
+ expect(splashpage.valid?).to be true
+ end
+
+ it 'validates video_url is a valid URL format' do
+ splashpage.video_url = 'not-a-url'
+ expect(splashpage.valid?).to be false
+ expect(splashpage.errors[:video_url]).to be_present
+ end
+
+ it 'validates video_url is from a supported platform' do
+ splashpage.video_url = 'https://example.com/video'
+ expect(splashpage.valid?).to be false
+ expect(splashpage.errors[:video_url]).to be_present
+ end
+
+ it 'accepts valid youtube.com/watch URLs' do
+ splashpage.video_url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
+ expect(splashpage.valid?).to be true
+ end
+
+ it 'accepts valid youtu.be URLs' do
+ splashpage.video_url = 'https://youtu.be/dQw4w9WgXcQ'
+ expect(splashpage.valid?).to be true
+ end
+
+ it 'accepts valid youtube.com/embed URLs' do
+ splashpage.video_url = 'https://www.youtube.com/embed/dQw4w9WgXcQ'
+ expect(splashpage.valid?).to be true
+ end
+
+ it 'accepts valid vimeo.com URLs' do
+ splashpage.video_url = 'https://vimeo.com/123456789'
+ expect(splashpage.valid?).to be true
+ end
+
+ it 'accepts valid PeerTube URLs' do
+ splashpage.video_url = 'https://peertube.example.com/videos/watch/abc-123-def'
+ expect(splashpage.valid?).to be true
+ end
+
+ it 'accepts valid Invidious URLs' do
+ splashpage.video_url = 'https://invidious.example.com/watch?v=dQw4w9WgXcQ'
+ expect(splashpage.valid?).to be true
+ end
+ end
+ end
+
+ describe '#video_platform' do
+ it 'detects YouTube from youtube.com/watch URL' do
+ splashpage.video_url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
+ expect(splashpage.video_platform).to eq(:youtube)
+ end
+
+ it 'detects YouTube from youtu.be URL' do
+ splashpage.video_url = 'https://youtu.be/dQw4w9WgXcQ'
+ expect(splashpage.video_platform).to eq(:youtube)
+ end
+
+ it 'detects YouTube from youtube.com/embed URL' do
+ splashpage.video_url = 'https://www.youtube.com/embed/dQw4w9WgXcQ'
+ expect(splashpage.video_platform).to eq(:youtube)
+ end
+
+ it 'detects YouTube from m.youtube.com URL' do
+ splashpage.video_url = 'https://m.youtube.com/watch?v=dQw4w9WgXcQ'
+ expect(splashpage.video_platform).to eq(:youtube)
+ end
+
+ it 'detects Vimeo from vimeo.com URL' do
+ splashpage.video_url = 'https://vimeo.com/123456789'
+ expect(splashpage.video_platform).to eq(:vimeo)
+ end
+
+ it 'detects Vimeo from vimeo.com/video URL' do
+ splashpage.video_url = 'https://vimeo.com/video/123456789'
+ expect(splashpage.video_platform).to eq(:vimeo)
+ end
+
+ it 'detects PeerTube from videos/watch URL' do
+ splashpage.video_url = 'https://peertube.example.com/videos/watch/abc-123-def'
+ expect(splashpage.video_platform).to eq(:peertube)
+ end
+
+ it 'detects PeerTube from /w/ URL' do
+ splashpage.video_url = 'https://peertube.example.com/w/abc-123-def'
+ expect(splashpage.video_platform).to eq(:peertube)
+ end
+
+ it 'detects Invidious from watch URL' do
+ splashpage.video_url = 'https://invidious.example.com/watch?v=dQw4w9WgXcQ'
+ expect(splashpage.video_platform).to eq(:invidious)
+ end
+
+ it 'returns nil for unsupported platform' do
+ splashpage.video_url = 'https://dailymotion.com/video/x12345'
+ expect(splashpage.video_platform).to be_nil
+ end
+
+ it 'returns nil when video_url is blank' do
+ splashpage.video_url = nil
+ expect(splashpage.video_platform).to be_nil
+ end
+ end
+
+ describe '#video_id' do
+ context 'YouTube' do
+ it 'extracts video ID from youtube.com/watch URL' do
+ splashpage.video_url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
+ expect(splashpage.video_id).to eq('dQw4w9WgXcQ')
+ end
+
+ it 'extracts video ID from youtu.be URL' do
+ splashpage.video_url = 'https://youtu.be/dQw4w9WgXcQ'
+ expect(splashpage.video_id).to eq('dQw4w9WgXcQ')
+ end
+
+ it 'extracts video ID from youtube.com/embed URL' do
+ splashpage.video_url = 'https://www.youtube.com/embed/dQw4w9WgXcQ'
+ expect(splashpage.video_id).to eq('dQw4w9WgXcQ')
+ end
+
+ it 'handles URLs with additional query parameters' do
+ splashpage.video_url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=10s'
+ expect(splashpage.video_id).to eq('dQw4w9WgXcQ')
+ end
+ end
+
+ context 'Vimeo' do
+ it 'extracts video ID from vimeo.com URL' do
+ splashpage.video_url = 'https://vimeo.com/123456789'
+ expect(splashpage.video_id).to eq('123456789')
+ end
+
+ it 'extracts video ID from vimeo.com/video URL' do
+ splashpage.video_url = 'https://vimeo.com/video/123456789'
+ expect(splashpage.video_id).to eq('123456789')
+ end
+ end
+
+ context 'PeerTube' do
+ it 'extracts video ID from videos/watch URL' do
+ splashpage.video_url = 'https://peertube.example.com/videos/watch/abc-123-def'
+ expect(splashpage.video_id).to eq('abc-123-def')
+ end
+
+ it 'extracts video ID from /w/ URL' do
+ splashpage.video_url = 'https://peertube.example.com/w/xyz-456-ghi'
+ expect(splashpage.video_id).to eq('xyz-456-ghi')
+ end
+ end
+
+ context 'Invidious' do
+ it 'extracts video ID from Invidious URL' do
+ splashpage.video_url = 'https://invidious.example.com/watch?v=dQw4w9WgXcQ'
+ expect(splashpage.video_id).to eq('dQw4w9WgXcQ')
+ end
+ end
+
+ it 'returns nil when video_url is blank' do
+ splashpage.video_url = nil
+ expect(splashpage.video_id).to be_nil
+ end
+
+ it 'returns nil for unsupported platform' do
+ splashpage.video_url = 'https://example.com/video'
+ expect(splashpage.video_id).to be_nil
+ end
+ end
+
+ describe '#video_instance_url' do
+ it 'extracts instance URL from PeerTube videos/watch URL' do
+ splashpage.video_url = 'https://peertube.example.com/videos/watch/abc-123-def'
+ expect(splashpage.video_instance_url).to eq('https://peertube.example.com')
+ end
+
+ it 'extracts instance URL from PeerTube /w/ URL' do
+ splashpage.video_url = 'https://peertube.example.com/w/abc-123-def'
+ expect(splashpage.video_instance_url).to eq('https://peertube.example.com')
+ end
+
+ it 'extracts instance URL from Invidious URL' do
+ splashpage.video_url = 'https://invidious.example.com/watch?v=dQw4w9WgXcQ'
+ expect(splashpage.video_instance_url).to eq('https://invidious.example.com')
+ end
+
+ it 'returns nil for YouTube URL' do
+ splashpage.video_url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
+ expect(splashpage.video_instance_url).to be_nil
+ end
+
+ it 'returns nil for Vimeo URL' do
+ splashpage.video_url = 'https://vimeo.com/123456789'
+ expect(splashpage.video_instance_url).to be_nil
+ end
+
+ it 'returns nil when video_url is blank' do
+ splashpage.video_url = nil
+ expect(splashpage.video_instance_url).to be_nil
+ end
+ end
+
+ describe '#video_embed_url' do
+ it 'generates YouTube embed URL with autoplay parameters' do
+ splashpage.video_url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
+ expect(splashpage.video_embed_url).to eq('https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1&loop=1&mute=1&controls=0&disablekb=1&fs=0&iv_load_policy=3&modestbranding=1&playsinline=1')
+ end
+
+ it 'generates Vimeo embed URL with autoplay parameters' do
+ splashpage.video_url = 'https://vimeo.com/123456789'
+ expect(splashpage.video_embed_url).to eq('https://player.vimeo.com/video/123456789?autoplay=1&muted=1&loop=1&playsinline=1')
+ end
+
+ it 'generates PeerTube embed URL with autoplay' do
+ splashpage.video_url = 'https://peertube.example.com/videos/watch/abc-123-def'
+ expect(splashpage.video_embed_url).to eq('https://peertube.example.com/videos/embed/abc-123-def?autoplay=1&loop=1')
+ end
+
+ it 'generates Invidious embed URL with autoplay' do
+ splashpage.video_url = 'https://invidious.example.com/watch?v=dQw4w9WgXcQ'
+ expect(splashpage.video_embed_url).to eq('https://invidious.example.com/embed/dQw4w9WgXcQ?autoplay=1&loop=1')
+ end
+
+ it 'returns nil when video_url is blank' do
+ splashpage.video_url = nil
+ expect(splashpage.video_embed_url).to be_nil
+ end
+
+ it 'returns nil for unsupported platform' do
+ splashpage.video_url = 'https://example.com/video'
+ expect(splashpage.video_embed_url).to be_nil
+ end
+ end
+end