Skip to content

Commit 707d545

Browse files
robertbpugharcangeliniclaudeCopilotCopilot
authored
Podcast: add episode block (#48546)
* Add Podcast Episode block * Podcast Episode: split ternary __() calls so i18n extractor can read string literals * Podcast Episode: post-bound alternate (one source of truth for title/cover/excerpt) Alternate model that drops the duplicating attributes (title, summary, description, author, imageId, imageUrl, publishDate) and reads them from the surrounding post instead. - block.json: remove title/summary/description/author/imageId/imageUrl/ publishDate attributes. Block keeps only audio + episode + Podcasting 2.0 metadata. - edit.js: useSelect from core/editor reads post title, excerpt, featured image, author. Sidebar collapses to three panels (Episode, Audio, Podcasting 2.0). Placeholder when block lives outside a post/page. - podcast-episode.php: render_callback uses get_the_title(), get_the_excerpt(), get_the_post_thumbnail_url(), get_the_author(), get_the_date(). Guarded by is_singular() so the block stays inert in sidebars and template parts. - save.js: drop title fallback in the noscript link. Why: previously the block stored its own copy of the post title, featured image, and excerpt. Authors edit the post, the block goes stale; or they edit the block, the post stays out of date. RSS, Reader, the block, and the post page can each show different copy of the same episode. Substack and every episode-centric platform avoids this by treating the post as the episode. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Podcast Episode: use block context, reuse convertSecondsToTimeCode, restore poster size - Read post title/excerpt/featured image/author/date via useEntityProp with usesContext: ['postId', 'postType', 'queryId']. Same pattern as core's post-title/post-excerpt/post-featured-image blocks. Block now works inside Query Loops and site-editor singular templates, not just the post editor. - Replace local formatSeconds() with convertSecondsToTimeCode from extensions/shared/components/media-player-control/utils. - Render publish date in editor preview to match the frontend. - Use medium_large (768px) for the poster image instead of large (1024px); the cover renders at 256-512px CSS. - Hoist person-row inline style to a module const. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Podcast Episode: read post via block context, defer mejs lookup, tighten i18n + a11y - render_block() now accepts \WP_Block and resolves the backing post from block context, falling back to the global loop. Drops the is_singular() guard so the block works inside Query Loops and feeds, and switches all template tags to post-aware variants (get_the_title($post), etc.) so the rendered episode matches the loop item, not whatever the global page query points at. - utils.js (extensions/shared/components/media-player-control): wrap the mejs.Utils helpers in arrow functions so the lookup happens at call time. Reading them at module-evaluation time would throw ReferenceError if the importing block evaluates before mediaelement loads on the page. - Editor: switch concatenated Season/Episode strings to sprintf( __( 'Season %d' ) ) so locales that reorder noun/number can translate cleanly. Drop `alt={ postTitle }` on the cover-art preview (the title is rendered immediately after as h3), use BaseControl .VisualLabel for the People section so the label does not point at a non-existent input id, gate the placeholder on both postId AND postType, and drop the redundant useBlockProps className argument. - Cover-art alt also dropped on the frontend for the same a11y reason. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Podcast Episode: episode cover art override, drop excerpt/GUID UI Cover art now falls back show -> episode override (Apple/Spotify hierarchy). Featured image is no longer reused, so themes don't render the same image twice. Show notes hint replaces the excerpt summary on the player card so authors write notes once, in post content. GUID UI dropped - RSS layer derives it from the permalink. * Podcast Episode: fix CI lint/phpcs/phan/build errors - Prettier: collapse multi-line help, VisualLabel children, ternary args. - i18n: move translator comments inside sprintf so the rule reads them. - i18n: split __() ternary so the extractor sees literal msgids. - PHPCS: pass JSON encoding flags to wp_json_encode. - Phan: cast post_author to int for get_the_author_meta. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Address PR review comments: coverArt guard, TRANSCRIPT_TYPE_OPTIONS i18n, PHPUnit tests Agent-Logs-Url: https://github.com/Automattic/jetpack/sessions/fb9995d0-fe14-48cb-9045-c12c05d7b263 Co-authored-by: robertbpugh <52668747+robertbpugh@users.noreply.github.com> * Podcast Episode: use get_block_wrapper_attributes for class+style output * Podcast Episode: drop get_block_wrapper_attributes, fix PHPCS alignment get_block_wrapper_attributes() depends on WP_Block_Supports::$block_to_render state that is null when render_block is invoked directly from PHPUnit, so the three coverArt-fallback tests crashed with "Trying to access array offset on value of type null". Switch back to a plain class attribute escaped through esc_attr(); production output is identical and the PHPCS escaper rule is satisfied without an ignore comment. Also fix PHPCS MultipleStatementAlignment warnings on the $original_post / $GLOBALS['post'] pair in test_no_post_context_returns_empty_string. * Podcast Episode: use null coalescing for $original_post (Phan) * Podcast Episode: ship from jetpack-podcast package, gate behind untangle filter * Podcast Episode: drop inline-data global, get_block_wrapper_attributes, TSX migration * Podcast Episode: review nits (drop unused const, _x context, echo wrapper attrs) * Podcast Episode: migrate remaining block utils + icons to TypeScript * Podcast Episode: refresh wpcomsh composer.lock for new podcast deps * Podcast Episode: address review feedback + mixed-content fix - Drop unused mediaSize attribute from block.json, edit.tsx - Richer author microdata (Schema.org Person with nested name) - Include explicit flag in meta-line gate (was missing in PHP, matched JSX) - Remove get_block_wrapper_attributes test-fallback; tests prime WP_Block_Supports::$block_to_render instead - Add happy-path render tests: title/author/date, video vs audio, trailer/bonus/explicit badges, people list, transcript/chapters/location/license links - Force admin scheme on the editor script URL to avoid mixed-content blocks on WPCOM sites whose mapped custom domain returns http via home_url() * Podcast Episode: revert media-player-control hardening The podcast package keeps the lazy-mejs lookup in its own local util/time-code.ts copy, so this PR no longer needs to touch the Jetpack plugin's shared util. Slim the Jetpack changelog to describe only the extensions/index.json registration. * Podcast Episode: mark dist/ as production-include in podcast package Without this flag, jetpack rsync and CI mirror builds were skipping the package's compiled editor JS (`dist/blocks/podcast-episode/editor.js`) because dist/ is gitignored. Also production-exclude the source TS/SCSS since the compiled versions are what ships. * Podcast Episode: use script_loader_src filter for admin-scheme rewrite * Refresh composer.lock for jetpack + mu-wpcom-plugin after podcast deps * mu-wpcom-plugin: add changelog for podcast package bump * Podcast Episode: add soundbite + alternateEnclosure attrs; slim local util copies * Podcast Episode: render soundbites at startTime=0 (skip on missing key, not on zero) * Podcast Episode: ship frontend CSS, inline chapters, cover-art picker refresh - Split style.scss into its own webpack entry so the block's shared CSS ships on the public post page; register the frontend style handle via Assets::register_script and hand it to register_block_type via the `style` arg. - Move script_loader_src filter registration into load_editor_scripts so it no longer fires on every front-end script load. - Add is_array($person) guard in the people render loop to match the soundbites / alternateEnclosures patterns. - Replace chaptersUrl (string) with a chapters array of { startTime, title } objects; add a ChaptersEditor in the Inspector, render an ordered chapter list (sorted by startTime) on the frontend, and remove the legacy "View chapters" link. - Refresh the cover-art picker into a featured-image-style full-width clickable preview with Replace / Remove actions and an empty-state dashed button. - Reorganize the Inspector: Audio panel hosts transcript + chapters, third panel renames to Metadata. - Block_Test: construct a real WP_Block in block_ctx() so phan stops complaining about \stdClass vs \WP_Block at the render_block call sites; add a chapter render test and drop the obsolete chaptersUrl assertion. * Podcast Episode: use schema.org contributor for people (not actor) --------- Co-authored-by: Tony Arcangelini <tony@arcangelini.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 32b9c67 commit 707d545

30 files changed

Lines changed: 2304 additions & 29 deletions

pnpm-lock.yaml

Lines changed: 54 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

projects/packages/podcast/.gitattributes

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ package.json export-ignore
55

66
# Files to include in the mirror repo, but excluded via gitignore
77
# Remember to end all directories with `/**` to properly tag every file.
8-
/build/** production-include
8+
/build/** production-include
9+
/dist/** production-include
910

1011
# Files to exclude from the mirror repo, but included in the monorepo.
1112
# Remember to end all directories with `/**` to properly tag every file.
12-
.gitignore production-exclude
13-
changelog/** production-exclude
14-
.phpcs.dir.xml production-exclude
15-
tests/** production-exclude
16-
.phpcsignore production-exclude
17-
routes/**/*.tsx production-exclude
13+
.gitignore production-exclude
14+
babel.config.js production-exclude
15+
changelog/** production-exclude
16+
.phpcs.dir.xml production-exclude
17+
tests/** production-exclude
18+
.phpcsignore production-exclude
19+
routes/**/*.tsx production-exclude
20+
webpack.config.blocks.js production-exclude
21+
/src/blocks/**/*.ts production-exclude
22+
/src/blocks/**/*.tsx production-exclude
23+
/src/blocks/**/*.scss production-exclude

projects/packages/podcast/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ vendor/
22
node_modules/
33
.cache/
44
build/
5+
dist/
56
composer.lock
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const config = {
2+
presets: [
3+
[
4+
'@automattic/jetpack-webpack-config/babel/preset',
5+
{ pluginReplaceTextdomain: { textdomain: 'jetpack-podcast' } },
6+
],
7+
],
8+
};
9+
10+
export default config;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Add the Podcast Episode block. Embeds a single podcast episode from an audio or video file with Podcasting 2.0 metadata. Registration is gated behind the `jetpack_podcast_untangle` filter (default off).

projects/packages/podcast/composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"license": "GPL-2.0-or-later",
66
"require": {
77
"php": ">=7.2",
8+
"automattic/jetpack-assets": "@dev",
9+
"automattic/jetpack-blocks": "@dev",
810
"automattic/jetpack-status": "@dev",
911
"automattic/jetpack-wp-build-polyfills": "@dev"
1012
},
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1-
import { makeBaseConfig } from 'jetpack-js-tools/eslintrc/base.mjs';
1+
import { defineConfig, makeBaseConfig } from 'jetpack-js-tools/eslintrc/base.mjs';
22

3-
export default makeBaseConfig( import.meta.url );
3+
export default defineConfig( makeBaseConfig( import.meta.url ), {
4+
// Block-editor sources follow the same conventions as the Jetpack
5+
// plugin's `extensions/` directory: inline JSX handlers are common, and
6+
// functional React components don't carry JSDoc.
7+
files: [ 'src/blocks/**/*.{js,jsx,ts,tsx,mjs,cjs}' ],
8+
rules: {
9+
'react/jsx-no-bind': 'off',
10+
'jsdoc/require-jsdoc': 'off',
11+
'jsdoc/require-description': 'off',
12+
'jsdoc/require-param-description': 'off',
13+
'jsdoc/require-param-type': 'off',
14+
'jsdoc/require-returns': 'off',
15+
},
16+
} );

projects/packages/podcast/package.json

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,55 @@
1616
"author": "Automattic",
1717
"type": "module",
1818
"scripts": {
19-
"build": "pnpm run clean && pnpm run build:wp-build && pnpm run build:boot-asset",
19+
"build": "pnpm run clean && pnpm run build:blocks && pnpm run build:wp-build && pnpm run build:boot-asset",
20+
"build:blocks": "webpack --config ./webpack.config.blocks.js",
2021
"build:boot-asset": "provide-boot-asset-file",
2122
"build:wp-build": "wp-build",
22-
"build-production": "pnpm run clean && pnpm run build:wp-build && pnpm run build:boot-asset",
23-
"clean": "rm -rf build/",
24-
"watch": "wp-build --watch"
23+
"build-production": "NODE_ENV=production BABEL_ENV=production pnpm run clean && pnpm run build:blocks && pnpm run build:wp-build && pnpm run build:boot-asset",
24+
"clean": "rm -rf build/ dist/ .cache/",
25+
"watch": "concurrently 'pnpm:build:blocks --watch' 'pnpm:watch:wp-build'",
26+
"watch:wp-build": "wp-build --watch"
2527
},
2628
"browserslist": [
2729
"extends @wordpress/browserslist-config"
2830
],
2931
"dependencies": {
32+
"@automattic/babel-plugin-replace-textdomain": "workspace:*",
3033
"@automattic/charts": "workspace:*",
3134
"@automattic/jetpack-analytics": "workspace:*",
3235
"@automattic/jetpack-components": "workspace:*",
3336
"@automattic/jetpack-script-data": "workspace:*",
37+
"@automattic/jetpack-shared-extension-utils": "workspace:*",
38+
"@automattic/jetpack-webpack-config": "workspace:*",
3439
"@automattic/number-formatters": "workspace:*",
3540
"@wordpress/api-fetch": "7.44.0",
41+
"@wordpress/block-editor": "15.17.0",
42+
"@wordpress/blocks": "15.17.0",
3643
"@wordpress/components": "32.6.0",
3744
"@wordpress/compose": "7.44.0",
3845
"@wordpress/core-data": "7.44.0",
3946
"@wordpress/data": "10.44.0",
4047
"@wordpress/dataviews": "14.1.0",
48+
"@wordpress/date": "5.44.0",
49+
"@wordpress/dependency-extraction-webpack-plugin": "6.44.0",
4150
"@wordpress/element": "6.44.0",
51+
"@wordpress/hooks": "4.44.0",
4252
"@wordpress/html-entities": "4.44.0",
4353
"@wordpress/i18n": "6.17.0",
4454
"@wordpress/icons": "12.2.0",
4555
"@wordpress/media-utils": "5.44.0",
4656
"@wordpress/notices": "5.44.0",
57+
"@wordpress/primitives": "4.44.0",
4758
"@wordpress/route": "0.10.0",
4859
"@wordpress/ui": "0.11.0",
4960
"@wordpress/url": "4.44.0",
50-
"canvas-confetti": "1.9.4"
61+
"canvas-confetti": "1.9.4",
62+
"clsx": "2.1.1",
63+
"copy-webpack-plugin": "14.0.0",
64+
"react": "18.3.1",
65+
"react-dom": "18.3.1",
66+
"webpack": "5.105.2",
67+
"webpack-cli": "6.0.1"
5168
},
5269
"devDependencies": {
5370
"@automattic/jetpack-base-styles": "workspace:*",
@@ -60,11 +77,10 @@
6077
"@wordpress/browserslist-config": "6.44.0",
6178
"@wordpress/build": "0.13.0",
6279
"@wordpress/theme": "0.11.0",
63-
"browserslist": "^4.24.0"
64-
},
65-
"optionalDependencies": {
66-
"react": "18.3.1",
67-
"react-dom": "18.3.1"
80+
"browserslist": "^4.24.0",
81+
"concurrently": "9.2.1",
82+
"sass-embedded": "1.97.3",
83+
"sass-loader": "16.0.5"
6884
},
6985
"wpPlugin": {
7086
"name": "jetpack_podcast",
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
{
2+
"$schema": "https://schemas.wp.org/trunk/block.json",
3+
"apiVersion": 3,
4+
"name": "jetpack/podcast-episode",
5+
"title": "Podcast Episode",
6+
"description": "Embed a single podcast episode from an audio or video file, with Podcasting 2.0 metadata.",
7+
"keywords": [ "audio", "podcast", "episode" ],
8+
"version": "1.0.0",
9+
"textdomain": "jetpack-podcast",
10+
"category": "embed",
11+
"icon": "<svg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg'><path d='M12 2a5 5 0 0 0-5 5v5a5 5 0 0 0 10 0V7a5 5 0 0 0-5-5zm0 2a3 3 0 0 1 3 3v5a3 3 0 0 1-6 0V7a3 3 0 0 1 3-3zm-7 8a1 1 0 0 1 1 1 6 6 0 0 0 12 0 1 1 0 1 1 2 0 8 8 0 0 1-7 7.93V22h3v2H8v-2h3v-1.07A8 8 0 0 1 4 13a1 1 0 0 1 1-1z'/></svg>",
12+
"usesContext": [ "postId", "postType", "queryId" ],
13+
"supports": {
14+
"spacing": {
15+
"padding": true,
16+
"margin": true
17+
},
18+
"anchor": true,
19+
"customClassName": true,
20+
"className": true,
21+
"html": false,
22+
"multiple": true,
23+
"reusable": true
24+
},
25+
"attributes": {
26+
"mediaId": {
27+
"type": "integer"
28+
},
29+
"mediaUrl": {
30+
"type": "string"
31+
},
32+
"mediaType": {
33+
"type": "string",
34+
"enum": [ "audio", "video" ]
35+
},
36+
"mediaMimeType": {
37+
"type": "string"
38+
},
39+
"episodeNumber": {
40+
"type": "integer"
41+
},
42+
"seasonNumber": {
43+
"type": "integer"
44+
},
45+
"episodeType": {
46+
"type": "string",
47+
"enum": [ "full", "trailer", "bonus" ],
48+
"default": "full"
49+
},
50+
"explicit": {
51+
"type": "boolean",
52+
"default": false
53+
},
54+
"duration": {
55+
"type": "string",
56+
"default": ""
57+
},
58+
"transcriptUrl": {
59+
"type": "string",
60+
"default": ""
61+
},
62+
"transcriptType": {
63+
"type": "string",
64+
"enum": [ "text/vtt", "text/html", "application/srt", "application/json" ],
65+
"default": "text/vtt"
66+
},
67+
"chapters": {
68+
"type": "array",
69+
"default": []
70+
},
71+
"locationName": {
72+
"type": "string",
73+
"default": ""
74+
},
75+
"license": {
76+
"type": "string",
77+
"default": ""
78+
},
79+
"licenseUrl": {
80+
"type": "string",
81+
"default": ""
82+
},
83+
"people": {
84+
"type": "array",
85+
"default": []
86+
},
87+
"showPoster": {
88+
"type": "boolean",
89+
"default": true
90+
},
91+
"coverArt": {
92+
"type": "object",
93+
"default": {}
94+
},
95+
"soundbites": {
96+
"type": "array",
97+
"default": []
98+
},
99+
"alternateEnclosures": {
100+
"type": "array",
101+
"default": []
102+
}
103+
},
104+
"example": {
105+
"attributes": {
106+
"episodeNumber": 1,
107+
"seasonNumber": 1,
108+
"episodeType": "full",
109+
"duration": "11:25"
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)