Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion .github/workflows/pot_up_to_date_on_pull_request.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Only PRs into main (the develop → main release and patch releases) need a
# fresh POT; feature PRs into develop don't have to regenerate it.
on:
pull_request:
branches:
- develop
- main

name: Translation Template
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog #

## v2.5.0 (October 22, 2026) ##

- Feat: GoDAM can add a video sitemap that lists every page with a GoDAM video, through Yoast SEO, Rank Math, All in One SEO or the WordPress sitemap. Turn it on in GoDAM > Settings > SEO; single videos can be left out.
- Feat: Video chapters are added to the video's search data as key moments, so Google can show links that jump to each chapter.
- Tweak: The video schema GoDAM adds for search engines now uses a video URL Google can index, adds the player's embed link, and leaves out empty fields.
- Tweak: Editors see a warning when a video's title looks like a raw file name.
- Tweak: The WPBakery GoDAM Video element's SEO Settings tab shows the same Video SEO fields as the block editor and Elementor.
- Tweak: The video share popup now shares the page link. The GoDAM Central and Embed links are gone, and the share button shows on every video when sharing is turned on.
- Tweak: GoDAM no longer adds the signed-in viewer's email address to the page or to video analytics.
- Fix: Audio converted to MP3 now shows in the block editor's audio list, including files converted before this update.
- Fix: The Gravity Forms Record field keeps working after an AJAX page change or a failed submit.
- Fix: The WordPress admin menu no longer flashes and jumps when a GoDAM page loads.
- Fix: The form layer preview in the video editor now accepts only a valid form ID.

## v2.4.1 (October 20, 2026) ##

- Fix: The video transcript no longer covers the video. It opens beside the video, or below it on narrow screens, matches the player skin, and can be searched.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Tested up to: 7.1

Requires PHP: 7.4

Stable tag: 2.4.1
Stable tag: 2.5.0

License: [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html)

Expand Down
7 changes: 4 additions & 3 deletions admin/class-rtgodam-transcoder-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ public function add_media_thumbnails( $post_array ) {
* @param string $job_for Whether media uploaded through rtmedia plugin or WordPress media.
*/
public function add_transcoded_files( $file_post_array, $attachment_id, $job_for = '' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
$transcoded_files = false;
$transcoded_files = array();
global $wpdb;

do_action( 'rtgodam_before_transcoded_media_store', $attachment_id, $file_post_array );
Expand All @@ -899,7 +899,7 @@ public function add_transcoded_files( $file_post_array, $attachment_id, $job_for

$download_url = urldecode( urldecode( $file ) );
$new_wp_attached_file_pathinfo = pathinfo( $download_url );
$post_mime_type = 'mp4' === $new_wp_attached_file_pathinfo['extension'] ? 'video/mp4' : 'audio/mp3';
$post_mime_type = 'mp4' === $new_wp_attached_file_pathinfo['extension'] ? 'video/mp4' : 'audio/mpeg';
$attachemnt_url = wp_get_attachment_url( $attachment_id );

$timeout = 5;
Expand Down Expand Up @@ -958,7 +958,8 @@ public function add_transcoded_files( $file_post_array, $attachment_id, $job_for
wp_update_post( // godam-coverage-ignore -- add_transcoded_files(): covered transitively — sole caller (handle_wp_media_transcoding_callback) already runs inside its own caller's before/after pair.
array(
'ID' => $attachment_id,
'post_mime_type' => 'audio/mp3',
// WordPress registers MP3 as audio/mpeg; audio/mp3 is missed by REST media_type=audio and wp_get_mime_types().
'post_mime_type' => 'audio/mpeg',
)
);
} else {
Expand Down
7 changes: 0 additions & 7 deletions admin/godam-transcoder-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -736,16 +736,9 @@ function rtgodam_get_localize_array() {
* Get Current User.
*/
$current_user = wp_get_current_user();
$email_id = '';

$localize_array['userId'] = $current_user->ID;

if ( ! empty( (array) $current_user->data ) ) {

$email_id = $current_user->data->user_email;
$localize_array['emailId'] = $email_id;
}

$localize_array['siteId'] = get_current_blog_id();

if ( get_post_type() ) { // godam-coverage-ignore -- No argument: same current-page-type check as postTitle above, not an explicit attachment lookup.
Expand Down
4 changes: 4 additions & 0 deletions assets/src/blocks/godam-player/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@
"type": "boolean",
"default": false
},
"excludeFromVideoSitemap": {
"type": "boolean",
"default": false
},
"verticalAlignment": {
"type": "string",
"default": "center"
Expand Down
34 changes: 30 additions & 4 deletions assets/src/blocks/godam-player/components/VideoSEOModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import apiFetch from '@wordpress/api-fetch';
* Internal dependencies
*/
import './video-seo-modal.scss';
import { appendTimezoneOffsetToUTC, stripHtmlTags } from '../utils';
import { appendTimezoneOffsetToUTC, stripHtmlTags, getFileNameFromUrl, isFilenameLikeTitle } from '../utils';

/**
* Video SEO Modal component
Expand All @@ -39,6 +39,8 @@ export default function VideoSEOModal( { isOpen, setIsOpen, attributes, setAttri
const [ mediaSEOData, setMediaSEOData ] = useState( null );
const [ isLoadingMediaSEO, setIsLoadingMediaSEO ] = useState( false );
const [ seoOverride, setSeoOverride ] = useState( attributes?.seoOverride || false );
const [ excludeFromSitemap, setExcludeFromSitemap ] = useState( !! attributes?.excludeFromVideoSitemap );
const [ mediaFileName, setMediaFileName ] = useState( getFileNameFromUrl( attributes?.src || '' ) );
const posterThumbnail = attributes?.poster || '';

/**
Expand All @@ -53,8 +55,9 @@ export default function VideoSEOModal( { isOpen, setIsOpen, attributes, setAttri
try {
const response = await apiFetch( { path: `/wp/v2/media/${ attributes.id }` } );

// Google indexes HLS (.m3u8) but not DASH (.mpd), so prefer the HLS stream.
const seoFromMedia = {
contentUrl: response.meta?.rtgodam_transcoded_url || response.source_url || '',
contentUrl: response.meta?.rtgodam_hls_transcoded_url || response.source_url || '',
headline: response.title?.rendered || '',
description: stripHtmlTags( response.description?.rendered || '' ),
uploadDate: appendTimezoneOffsetToUTC( response.date_gmt || '' ),
Expand All @@ -64,6 +67,9 @@ export default function VideoSEOModal( { isOpen, setIsOpen, attributes, setAttri
};

setMediaSEOData( seoFromMedia );
if ( response.source_url ) {
setMediaFileName( getFileNameFromUrl( response.source_url ) );
}
return seoFromMedia;
} catch ( error ) {
// eslint-disable-next-line no-console
Expand All @@ -82,6 +88,7 @@ export default function VideoSEOModal( { isOpen, setIsOpen, attributes, setAttri
// Sync local override state with attributes
const currentSeoOverride = attributes?.seoOverride || false;
setSeoOverride( currentSeoOverride );
setExcludeFromSitemap( !! attributes?.excludeFromVideoSitemap );

// If seoOverride is false, fetch and display fresh data from media library
if ( ! currentSeoOverride ) {
Expand Down Expand Up @@ -120,7 +127,7 @@ export default function VideoSEOModal( { isOpen, setIsOpen, attributes, setAttri

// Also fetch media SEO for comparison/reset purposes
fetchMediaSEOData();
}, [ attributes.seo, attributes?.seoOverride, attributes?.poster, isOpen, fetchMediaSEOData ] ); // Depend on seo attribute and modal state
}, [ attributes.seo, attributes?.seoOverride, attributes?.excludeFromVideoSitemap, attributes?.poster, isOpen, fetchMediaSEOData ] ); // Depend on seo attribute and modal state

const updateField = ( field, value ) => {
setVideoData( { ...videoData, [ field ]: value } );
Expand Down Expand Up @@ -181,6 +188,7 @@ export default function VideoSEOModal( { isOpen, setIsOpen, attributes, setAttri
setVideoData( mediaSEOData );
}
setSeoOverride( attributes?.seoOverride || false );
setExcludeFromSitemap( !! attributes?.excludeFromVideoSitemap );
};

const saveData = () => {
Expand All @@ -192,6 +200,7 @@ export default function VideoSEOModal( { isOpen, setIsOpen, attributes, setAttri
setAttributes( {
seo: finalVideoData,
seoOverride,
excludeFromVideoSitemap: excludeFromSitemap,
} );
closeModal();
};
Expand All @@ -203,6 +212,8 @@ export default function VideoSEOModal( { isOpen, setIsOpen, attributes, setAttri
// Check if fields should be editable (only when override is enabled)
const isFieldsDisabled = ! seoOverride;

const isHeadlineFilenameLike = isFilenameLikeTitle( videoData?.headline, mediaFileName );

return (
<Modal
title={ __( 'Video SEO Schema', 'godam' ) }
Expand Down Expand Up @@ -248,7 +259,7 @@ export default function VideoSEOModal( { isOpen, setIsOpen, attributes, setAttri
label={ __( 'Content URL', 'godam' ) }
value={ videoData?.contentUrl || '' }
onChange={ ( value ) => updateField( 'contentUrl', value ) }
help={ __( 'URL of the video content can be MOV, MP4, MPD. Example: https://www.example.com/video.mp4', 'godam' ) }
help={ __( 'URL of the video file or HLS (.m3u8) stream that search engines index. Example: https://www.example.com/video.mp4', 'godam' ) }
disabled={ true }
/>
<TextControl
Expand All @@ -259,6 +270,14 @@ export default function VideoSEOModal( { isOpen, setIsOpen, attributes, setAttri
help={ __( 'Title of the video', 'godam' ) }
disabled={ isFieldsDisabled }
/>
{ isHeadlineFilenameLike && (
<Notice status="warning" isDismissible={ false } className="godam-seo-modal__notice godam-seo-modal__notice--filename">
{ seoOverride
? __( 'This title looks like a file name. Search engines show it as the video title, so use a descriptive title with spaces instead.', 'godam' )
: __( 'This title looks like a file name. Search engines show it as the video title. Edit the video title in the Media Library, or turn on "Override default SEO" to set a descriptive title for this block.', 'godam' )
}
</Notice>
) }
<TextareaControl
className={ `godam-seo-modal__property${ ! videoData?.description?.trim() ? ' godam-seo-modal__property--warning' : '' }` }
label={ __( 'Description', 'godam' ) }
Expand Down Expand Up @@ -302,6 +321,13 @@ export default function VideoSEOModal( { isOpen, setIsOpen, attributes, setAttri
help={ __( 'Is the video suitable for all audiences?', 'godam' ) }
disabled={ isFieldsDisabled }
/>
<ToggleControl
className="godam-seo-modal__property"
label={ __( 'Exclude from video sitemap', 'godam' ) }
checked={ excludeFromSitemap }
onChange={ ( value ) => setExcludeFromSitemap( value ) }
help={ __( 'Leave this video out of the GoDAM video sitemap. The video schema on this page is not affected.', 'godam' ) }
/>

<div style={ { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '16px' } }>
<Button variant="tertiary" onClick={ closeModal }>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@
}
}

// "Title looks like a file name" warning under the Headline field:
// same gap below it as between the property boxes.
&__notice--filename {
margin-top: 0;
margin-bottom: 1rem;
}

&__property {

background-color: rgb(248, 249, 250);
Expand Down
7 changes: 4 additions & 3 deletions assets/src/blocks/godam-player/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ function VideoEdit( {
const response = await apiFetch( { path: `/wp/v2/media/${ id }` } );

const enhancedSEOData = {
contentUrl: response.meta?.rtgodam_transcoded_url || response.source_url || src || '',
contentUrl: response.meta?.rtgodam_hls_transcoded_url || response.source_url || src || '',
headline: response.title?.rendered || '',
description: stripHtmlTags( response.description?.rendered || '' ),
uploadDate: appendTimezoneOffsetToUTC( response.date_gmt || '' ),
Expand Down Expand Up @@ -535,8 +535,9 @@ function VideoEdit( {
}

// Create new SEO data from GoDAM media
// Prefer the HLS stream: Google indexes .m3u8 but not DASH .mpd.
const newSEOData = {
contentUrl: media?.url,
contentUrl: media?.hls_url || media?.url,
headline: media?.title || '',
description: stripHtmlTags( media?.description || '' ),
uploadDate: appendTimezoneOffsetToUTC( media?.date || '' ),
Expand Down Expand Up @@ -597,7 +598,7 @@ function VideoEdit( {

// Create new SEO data from WordPress media
const newSEOData = {
contentUrl: response.meta?.rtgodam_transcoded_url || response.source_url,
contentUrl: response.meta?.rtgodam_hls_transcoded_url || response.source_url,
headline: response.title?.rendered || '',
description: stripHtmlTags( response.description?.rendered || '' ),
uploadDate: appendTimezoneOffsetToUTC( response.date_gmt ),
Expand Down
113 changes: 112 additions & 1 deletion assets/src/blocks/godam-player/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,115 @@ function stripHtmlTags( html ) {
return doc.body.textContent || '';
}

export { getFirstNonEmpty, appendTimezoneOffsetToUTC, isObjectEmpty, secondsToISO8601, isSEODataEmpty, stripHtmlTags };
/**
* Get the file name (without folders or query string) from a URL or path.
*
* @param {string} url - URL or path.
* @return {string} File name, e.g. "my-video_final.mp4", or ''.
*/
function getFileNameFromUrl( url ) {
if ( ! url || typeof url !== 'string' ) {
return '';
}

const path = url.split( /[?#]/ )[ 0 ];
const name = path.substring( path.lastIndexOf( '/' ) + 1 );

try {
return decodeURIComponent( name );
} catch {
return name;
}
}

/**
* Remove a trailing file extension, e.g. "clip.final.mp4" -> "clip.final".
*
* @param {string} name - File name.
* @return {string} Name without the last extension.
*/
function stripFileExtension( name ) {
return String( name || '' ).replace( /\.[a-z0-9]{2,5}$/i, '' );
}

/**
* Normalize a title or file name for comparison: lower case, and runs of
* spaces, hyphens and underscores collapsed to "-".
*
* WordPress builds the title from the uploaded name with its spaces but saves
* the file with hyphens, e.g. title "Screen Recording 2024-01-15 at 10.30.45"
* and file "Screen-Recording-2024-01-15-at-10.30.45.mov"; both normalize to
* the same string.
*
* @param {string} value - Title or file name.
* @return {string} Normalized value.
*/
function normalizeForFileNameComparison( value ) {
return String( value || '' ).trim().toLowerCase()
.replace( /[\s_-]+/g, '-' )
.replace( /^-+|-+$/g, '' );
}

/**
* Whether a video title looks like a raw file name rather than a real title.
*
* A single hyphen or underscore is not enough on its own: real titles such as
* "Spider-Man", "COVID-19", "e-commerce", "Add-ons", "Q3_review" or
* "Paris-2024" have one. A title is flagged when it matches the video's file
* name (ignoring case, the extension, and the difference between spaces,
* hyphens and underscores) *and* also looks machine-made: no spaces, or a
* date or time in it ("Screen Recording 2024-01-15 at 10.30.45"). WordPress
* builds the title from the uploaded file name, so a well-named upload
* ("Product Demo.mp4" → "Product Demo") always matches its file and must
* not be flagged for that alone. A title with no spaces is also flagged when it
* has two or more hyphens/underscores ("product-demo_v2"), is a camera or
* phone file name ("IMG_2041", "DSC-0001", "PXL_20240115"), or ends in a
* video file extension ("clip.mp4").
*
* @param {string} title - Video title / SEO headline.
* @param {string} [fileName] - The video's file name or URL, if known.
* @return {boolean} True when the title looks like a file name.
*/
function isFilenameLikeTitle( title, fileName = '' ) {
const trimmed = String( title || '' ).trim();

if ( ! trimmed ) {
return false;
}

const baseName = getFileNameFromUrl( fileName );
if ( baseName ) {
const normalizedTitle = normalizeForFileNameComparison( trimmed );
const normalizedFile = normalizeForFileNameComparison( baseName );
const normalizedFileNoExtension = normalizeForFileNameComparison( stripFileExtension( baseName ) );

const looksGenerated = ! /\s/.test( trimmed ) || /\d{4}-\d{2}-\d{2}|\d{1,2}\.\d{2}\.\d{2}/.test( trimmed );

if ( looksGenerated && normalizedTitle && ( normalizedTitle === normalizedFile || normalizedTitle === normalizedFileNoExtension ) ) {
return true;
}
}

if ( /\s/.test( trimmed ) ) {
return false;
}

// Two or more separators, e.g. "product-demo_v2".
if ( ( trimmed.match( /[-_]/g ) || [] ).length >= 2 ) {
return true;
}

// Camera / phone naming, e.g. "IMG_2041", "DSC-0001", "VID_20240115", "PXL_20240115".
if ( /^(img|vid|dsc|dscn|mvi|pxl|mov|dji|gopr|gp)[-_]?\d{3,}$/i.test( trimmed ) ) {
return true;
}

// A single word ending in a video file extension, e.g. "IMG2041.mp4".
if ( /\.(mp4|mov|m4v|webm|mkv|avi|wmv|flv|mpe?g|3gp|ogv|m3u8|mpd)$/i.test( trimmed ) ) {
return true;
}

return false;
}

export { getFirstNonEmpty, appendTimezoneOffsetToUTC, isObjectEmpty, secondsToISO8601, isSEODataEmpty, stripHtmlTags, getFileNameFromUrl, isFilenameLikeTitle };
Loading
Loading