Skip to content
Open
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 functions.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?php

Check failure on line 1 in functions.php

View workflow job for this annotation

GitHub Actions / php / PHP (8.1)

The "wmf" prefix is too short. Short prefixes are not unique enough and may cause name collisions with other code.
/**
* Wikimedia Foundation functions and definitions.
*
Expand All @@ -18,7 +18,7 @@
* runs before the init hook. The init hook is too late for some features, such
* as indicating support for post thumbnails.
*/
function wmf_setup() {

Check failure on line 21 in functions.php

View workflow job for this annotation

GitHub Actions / php / PHP (8.1)

Functions declared in the global namespace by a theme/plugin should start with the theme/plugin prefix. Found: "wmf_setup".
// Add default posts and comments RSS feed links to head.
add_theme_support( 'automatic-feed-links' );

Expand Down Expand Up @@ -65,7 +65,7 @@
// Set up the WordPress core custom background feature.
add_theme_support(
'custom-background', apply_filters(
'wmf_custom_background_args', array(

Check failure on line 68 in functions.php

View workflow job for this annotation

GitHub Actions / php / PHP (8.1)

Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "wmf_custom_background_args".
'default-color' => 'ffffff',
'default-image' => '',
)
Expand Down Expand Up @@ -96,7 +96,7 @@
*
* @link https://developer.wordpress.org/themes/functionality/sidebars/#registering-a-sidebar
*/
function wmf_widgets_init() {

Check failure on line 99 in functions.php

View workflow job for this annotation

GitHub Actions / php / PHP (8.1)

Functions declared in the global namespace by a theme/plugin should start with the theme/plugin prefix. Found: "wmf_widgets_init".
register_sidebar(
array(
'name' => esc_html__( 'Sidebar', 'shiro-admin' ),
Expand All @@ -114,7 +114,7 @@
/**
* Enqueue scripts and styles.
*/
function wmf_scripts() {

Check failure on line 117 in functions.php

View workflow job for this annotation

GitHub Actions / php / PHP (8.1)

Functions declared in the global namespace by a theme/plugin should start with the theme/plugin prefix. Found: "wmf_scripts".
$style_version = md5_file( get_theme_file_path( 'style.css' ) );
$script_version = md5_file( get_theme_file_path( 'assets/dist/scripts.min.js' ) );

Expand Down Expand Up @@ -162,7 +162,7 @@
/**
* Adds Piwik Analytics to the footer of each page.
*/
function wmf_add_piwik_analytics() {

Check failure on line 165 in functions.php

View workflow job for this annotation

GitHub Actions / php / PHP (8.1)

Functions declared in the global namespace by a theme/plugin should start with the theme/plugin prefix. Found: "wmf_add_piwik_analytics".
?>
<!-- Matomo -->
<script type="text/javascript">
Expand All @@ -189,7 +189,7 @@
* @param string $post_type Current Post Type name.
* @return array Filtered args.
*/
function wmf_edit_page_post_type( $args, $post_type ) {

Check failure on line 192 in functions.php

View workflow job for this annotation

GitHub Actions / php / PHP (8.1)

Functions declared in the global namespace by a theme/plugin should start with the theme/plugin prefix. Found: "wmf_edit_page_post_type".
if ( 'page' === $post_type ) {
$args['publicly_queryable'] = true;
}
Expand All @@ -201,7 +201,7 @@
/**
* Enqueue admin scripts and styles.
*/
function wmf_admin_scripts() {

Check failure on line 204 in functions.php

View workflow job for this annotation

GitHub Actions / php / PHP (8.1)

Functions declared in the global namespace by a theme/plugin should start with the theme/plugin prefix. Found: "wmf_admin_scripts".
wp_enqueue_style(
'shiro-editor',
get_template_directory_uri() . '/assets/dist/admin/admin.css',
Expand Down Expand Up @@ -258,6 +258,7 @@
* Block editor functionality.
*/
require get_template_directory() . '/inc/editor/namespace.php';
require get_template_directory() . '/inc/editor/blocks/accordion.php';
require get_template_directory() . '/inc/editor/blocks/blog-post.php';
require get_template_directory() . '/inc/editor/blocks/donation-portal-form.php';
require get_template_directory() . '/inc/editor/blocks/double-heading.php';
Expand Down Expand Up @@ -291,6 +292,7 @@
WMF\Assets\bootstrap();
WMF\Editor\bootstrap();
WMF\Editor\HasBlockColumn\bootstrap();
WMF\Editor\Blocks\Accordion\bootstrap();
WMF\Editor\Blocks\BlogPost\bootstrap();
WMF\Editor\Blocks\InlineLanguages\bootstrap();
WMF\Editor\Blocks\DonationPortalForm\bootstrap();
Expand Down
157 changes: 157 additions & 0 deletions inc/editor/blocks/accordion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

Check failure on line 1 in inc/editor/blocks/accordion.php

View workflow job for this annotation

GitHub Actions / php / PHP (8.1)

The "wmf" prefix is too short. Short prefixes are not unique enough and may cause name collisions with other code.
/**
* Additional functionality for the shiro/accordion block.
*
* The accordion blocks are static (their markup is saved into post content), so
* there is no render callback to hook. This adds FAQPage structured data by
* reading the saved question/answer markup and appending an FAQPage node to
* Yoast's schema @graph. Google retired FAQ rich results in 2023, so this pays
* off as GEO / AI-citation structured data rather than a search rich snippet.
*
* @package shiro
*/

namespace WMF\Editor\Blocks\Accordion;

Check failure on line 14 in inc/editor/blocks/accordion.php

View workflow job for this annotation

GitHub Actions / php / PHP (8.1)

Namespaces declared by a theme/plugin should start with the theme/plugin prefix. Found: "WMF\Editor\Blocks\Accordion".

use DOMDocument;
use DOMNode;
use DOMXPath;

const BLOCK_NAME = 'shiro/accordion';

/**
* Bootstrap this block functionality.
*/
function bootstrap() {
// Yoast already owns the page @graph (Organization, BreadcrumbList, …), so
// append an FAQPage node to it rather than emitting a separate JSON-LD island.
add_filter( 'wpseo_schema_graph', __NAMESPACE__ . '\\add_faq_schema', 11, 2 );
}

/**
* Append an FAQPage node to the Yoast schema graph for singular pages that
* contain a shiro/accordion block.
*
* @param array $graph The Yoast schema graph.
* @param mixed $context Yoast Meta_Tags_Context for the current page.
* @return array The (possibly extended) schema graph.
*/
function add_faq_schema( $graph, $context = null ): array {
if ( ! is_array( $graph ) || ! is_singular() ) {
return $graph;
}

$post = get_post();
if ( ! $post || ! has_block( BLOCK_NAME, $post ) ) {
return $graph;
}

$questions = get_faq_items_from_content( $post->post_content ?? '' );
if ( empty( $questions ) ) {
return $graph;
}

$canonical = '';
if ( is_object( $context ) && ! empty( $context->canonical ) ) {
$canonical = $context->canonical;
}
if ( empty( $canonical ) ) {
$canonical = (string) get_permalink( $post );
}

$graph[] = [
'@type' => 'FAQPage',
'@id' => $canonical . '#faq',
'mainEntity' => array_map(
function ( $item ) {
return [
'@type' => 'Question',
'name' => $item['name'],
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => $item['answer'],
],
];
},
$questions
),
];

// Link the FAQPage to the page's WebPage node so it sits in the graph rather
// than floating; Yoast ids its WebPage node as <canonical>#webpage.
$graph[ array_key_last( $graph ) ]['mainEntityOfPage'] = [ '@id' => $canonical . '#webpage' ];

return $graph;
}

/**
* Extract question/answer pairs from saved accordion markup.
*
* Reads the static block HTML directly (mirroring inc/editor/blocks/toc.php),
* since sourced block attributes such as the accordion-item title are not
* available to parse_blocks() on the server.
*
* @param string $content Raw post content.
* @return array List of [ 'name' => string, 'answer' => string ] pairs.
*/
function get_faq_items_from_content( string $content ): array {
if ( trim( $content ) === '' ) {
return [];
}

$doc = new DOMDocument();

// Suppress libxml warnings for the theme's HTML fragments; see toc.php / #907.
libxml_use_internal_errors( true );
$doc->loadHTML( '<meta charset="UTF-8">' . $content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
libxml_clear_errors();

$xpath = new DOMXPath( $doc );

// Match the exact `accordion-item` class token (not accordion-item__title etc.).
$items = $xpath->query( "//*[contains(concat(' ', normalize-space(@class), ' '), ' accordion-item ')]" );

$faq = [];
if ( ! $items ) {
return $faq;
}

/* phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */
foreach ( $items as $item ) {
$title_nodes = $xpath->query( ".//*[contains(concat(' ', normalize-space(@class), ' '), ' accordion-item__title-text ')]", $item );
$answer_nodes = $xpath->query( ".//*[contains(concat(' ', normalize-space(@class), ' '), ' accordion-item__content ')]", $item );

if ( ! $title_nodes || ! $title_nodes->length || ! $answer_nodes || ! $answer_nodes->length ) {
continue;
}

$name = trim( preg_replace( '/\s+/', ' ', $title_nodes->item( 0 )->textContent ) );
$answer = trim( inner_html( $answer_nodes->item( 0 ) ) );

if ( $name === '' || $answer === '' ) {
continue;
}

$faq[] = [
'name' => $name,
'answer' => $answer,
];
}
/* phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */

return $faq;
}

/**
* Serialise the inner HTML of a DOM node (its children, not the node itself).
*
* @param DOMNode $node Node whose children to serialise.
* @return string Inner HTML.
*/
function inner_html( DOMNode $node ): string {
$html = '';
foreach ( $node->childNodes as $child ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$html .= $node->ownerDocument->saveHTML( $child ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
}
return $html;
}
Loading