From 5182167a4ae0197b3aea6bc80ae6cf18b519d3e0 Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Tue, 3 Mar 2026 22:00:18 +0100 Subject: [PATCH 01/14] Add website-list block --- package.json | 1 + src/block/website-list/block.json | 72 +++++ src/block/website-list/edit.js | 494 +++++++++++++++++++++++++++++ src/block/website-list/editor.scss | 9 + src/block/website-list/index.js | 33 ++ src/block/website-list/render.php | 46 +++ src/block/website-list/style.scss | 12 + src/block/website-list/view.js | 22 ++ 8 files changed, 689 insertions(+) create mode 100644 src/block/website-list/block.json create mode 100644 src/block/website-list/edit.js create mode 100644 src/block/website-list/editor.scss create mode 100644 src/block/website-list/index.js create mode 100644 src/block/website-list/render.php create mode 100644 src/block/website-list/style.scss create mode 100644 src/block/website-list/view.js diff --git a/package.json b/package.json index 285fb29..24f981a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "test:unit": "wp-scripts test-unit-js" }, "devDependencies": { + "@wordpress/icons": "^11.7.0", "@wordpress/scripts": "^31.4.0", "postcss-prefix-selector": "^2.1.1" }, diff --git a/src/block/website-list/block.json b/src/block/website-list/block.json new file mode 100644 index 0000000..3e69289 --- /dev/null +++ b/src/block/website-list/block.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "webring/website-list", + "version": "0.1.0", + "title": "Webring Website List", + "category": "widgets", + "icon": "smiley", + "description": "Example block scaffolded with Create Block tool.", + "example": {}, + "supports": { + "html": false + }, + "attributes": { + "categories": { + "type": "array", + "items": { + "type": "object" + } + }, + "postsToShow": { + "type": "number", + "default": 5 + }, + "postLayout": { + "type": "string", + "default": "list" + }, + "columns": { + "type": "number", + "default": 3 + }, + "order": { + "type": "string", + "default": "desc" + }, + "orderBy": { + "type": "string", + "default": "date" + }, + "displayFeaturedImage": { + "type": "boolean", + "default": false + }, + "featuredImageAlign": { + "type": "string", + "enum": [ "left", "center", "right" ] + }, + "featuredImageSizeSlug": { + "type": "string", + "default": "thumbnail" + }, + "featuredImageSizeWidth": { + "type": "number", + "default": 150 + }, + "featuredImageSizeHeight": { + "type": "number", + "default": 150 + }, + "addLinkToFeaturedImage": { + "type": "boolean", + "default": false + } + }, + "textdomain": "webring", + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "render": "file:./render.php", + "viewScript": "file:./view.js" +} diff --git a/src/block/website-list/edit.js b/src/block/website-list/edit.js new file mode 100644 index 0000000..c0f910c --- /dev/null +++ b/src/block/website-list/edit.js @@ -0,0 +1,494 @@ +/** + * WordPress dependencies + */ +import { + Placeholder, + QueryControls, + RadioControl, + RangeControl, + Spinner, + ToggleControl, + ToolbarGroup, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; +import { dateI18n, format, getSettings } from '@wordpress/date'; +import { + InspectorControls, + BlockControls, + __experimentalImageSizeControl as ImageSizeControl, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { + pin, + list, + grid, + alignNone, + positionLeft, + positionCenter, + positionRight, +} from '@wordpress/icons'; +import { store as coreStore } from '@wordpress/core-data'; +import { store as noticeStore } from '@wordpress/notices'; +import { useInstanceId } from '@wordpress/compose'; +import { createInterpolateElement } from '@wordpress/element'; +import ServerSideRender from '@wordpress/server-side-render'; + +/** + * Module Constants + */ +const CATEGORIES_LIST_QUERY = { + per_page: -1, + _fields: 'id,name', + context: 'view', +}; + +const imageAlignmentOptions = [ + { + value: 'none', + icon: alignNone, + label: __( 'None' ), + }, + { + value: 'left', + icon: positionLeft, + label: __( 'Left' ), + }, + { + value: 'center', + icon: positionCenter, + label: __( 'Center' ), + }, + { + value: 'right', + icon: positionRight, + label: __( 'Right' ), + }, +]; + +/** + * Retrieves the translation of text. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/ + */ +import { __, _x, sprintf } from '@wordpress/i18n'; + +/** + * React hook that is used to mark the block wrapper element. + * It provides all the necessary props like the class name. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops + */ +import { useBlockProps } from '@wordpress/block-editor'; + +/** + * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. + * Those files can contain any CSS code that gets applied to the editor. + * + * @see https://www.npmjs.com/package/@wordpress/scripts#using-css + */ +import './editor.scss'; + +function Controls( { attributes, setAttributes } ) { + const { + postsToShow, + order, + orderBy, + categories, + selectedAuthor, + displayFeaturedImage, + displayWebsiteUrl, + postLayout, + columns, + featuredImageAlign, + featuredImageSizeSlug, + featuredImageSizeWidth, + featuredImageSizeHeight, + addLinkToFeaturedImage, + } = attributes; + const { + imageSizes, + defaultImageWidth, + defaultImageHeight, + categoriesList, + } = useSelect( + ( select ) => { + const { getEntityRecords } = select( coreStore ); + const settings = select( blockEditorStore ).getSettings(); + + return { + defaultImageWidth: + settings.imageDimensions?.[ featuredImageSizeSlug ] + ?.width ?? 0, + defaultImageHeight: + settings.imageDimensions?.[ featuredImageSizeSlug ] + ?.height ?? 0, + imageSizes: settings.imageSizes, + categoriesList: getEntityRecords( + 'taxonomy', + 'webring_category', + CATEGORIES_LIST_QUERY + ), + }; + }, + [ featuredImageSizeSlug ] + ); + + const imageSizeOptions = imageSizes + .filter( ( { slug } ) => slug !== 'full' ) + .map( ( { name, slug } ) => ( { + value: slug, + label: name, + } ) ); + const categorySuggestions = + categoriesList?.reduce( + ( accumulator, category ) => ( { + ...accumulator, + [ category.name ]: category, + } ), + {} + ) ?? {}; + const selectCategories = ( tokens ) => { + const hasNoSuggestion = tokens.some( + ( token ) => + typeof token === 'string' && ! categorySuggestions[ token ] + ); + if ( hasNoSuggestion ) { + return; + } + // Categories that are already will be objects, while new additions will be strings (the name). + // allCategories nomalizes the array so that they are all objects. + const allCategories = tokens.map( ( token ) => { + return typeof token === 'string' + ? categorySuggestions[ token ] + : token; + } ); + // We do nothing if the category is not selected + // from suggestions. + if ( allCategories.includes( null ) ) { + return false; + } + setAttributes( { categories: allCategories } ); + }; + + return ( + <> + + + setAttributes( { + displayWebsiteUrl: false, + } ) + } + > + !! displayWebsiteUrl } + label={ __( 'Display website URL' ) } + onDeselect={ () => + setAttributes( { displayWebsiteUrl: false } ) + } + isShownByDefault + > + + setAttributes( { displayWebsiteUrl: value } ) + } + /> + + + + setAttributes( { + displayFeaturedImage: false, + featuredImageAlign: undefined, + featuredImageSizeSlug: 'thumbnail', + featuredImageSizeWidth: null, + featuredImageSizeHeight: null, + addLinkToFeaturedImage: false, + } ) + } + > + !! displayFeaturedImage } + label={ __( 'Display featured image' ) } + onDeselect={ () => + setAttributes( { displayFeaturedImage: false } ) + } + isShownByDefault + > + + setAttributes( { displayFeaturedImage: value } ) + } + /> + + { displayFeaturedImage && ( + <> + + featuredImageSizeSlug !== 'thumbnail' || + featuredImageSizeWidth !== null || + featuredImageSizeHeight !== null + } + label={ __( 'Image size' ) } + onDeselect={ () => + setAttributes( { + featuredImageSizeSlug: 'thumbnail', + featuredImageSizeWidth: null, + featuredImageSizeHeight: null, + } ) + } + isShownByDefault + > + { + const newAttrs = {}; + if ( value.hasOwnProperty( 'width' ) ) { + newAttrs.featuredImageSizeWidth = + value.width; + } + if ( value.hasOwnProperty( 'height' ) ) { + newAttrs.featuredImageSizeHeight = + value.height; + } + setAttributes( newAttrs ); + } } + slug={ featuredImageSizeSlug } + width={ featuredImageSizeWidth } + height={ featuredImageSizeHeight } + imageWidth={ defaultImageWidth } + imageHeight={ defaultImageHeight } + imageSizeOptions={ imageSizeOptions } + imageSizeHelp={ __( + 'Select the size of the source image.' + ) } + onChangeImage={ ( value ) => + setAttributes( { + featuredImageSizeSlug: value, + featuredImageSizeWidth: undefined, + featuredImageSizeHeight: undefined, + } ) + } + /> + + !! featuredImageAlign } + label={ __( 'Image alignment' ) } + onDeselect={ () => + setAttributes( { + featuredImageAlign: undefined, + } ) + } + isShownByDefault + > + + setAttributes( { + featuredImageAlign: + value !== 'none' + ? value + : undefined, + } ) + } + > + { imageAlignmentOptions.map( + ( { value, icon, label } ) => { + return ( + + ); + } + ) } + + + !! addLinkToFeaturedImage } + label={ __( 'Add link to featured image' ) } + onDeselect={ () => + setAttributes( { + addLinkToFeaturedImage: false, + } ) + } + isShownByDefault + > + + setAttributes( { + addLinkToFeaturedImage: value, + } ) + } + /> + + + ) } + + + setAttributes( { + order: 'desc', + orderBy: 'date', + postsToShow: 5, + categories: undefined, + selectedAuthor: undefined, + columns: 3, + } ) + } + > + + order !== 'desc' || + orderBy !== 'date' || + postsToShow !== 5 || + categories?.length > 0 || + !!selectedAuthor + } + label={ __( 'Sort and filter' ) } + onDeselect={ () => + setAttributes( { + order: 'desc', + orderBy: 'date', + postsToShow: 5, + categories: undefined, + selectedAuthor: undefined, + } ) + } + isShownByDefault + > + + setAttributes( { order: value } ) + } + onOrderByChange={ ( value ) => + setAttributes( { orderBy: value } ) + } + onNumberOfItemsChange={ ( value ) => + setAttributes( { postsToShow: value } ) + } + categorySuggestions={ categorySuggestions } + onCategoryChange={ selectCategories } + selectedCategories={ categories } + /> + + + { postLayout === 'grid' && ( + columns !== 3 } + label={ __( 'Columns' ) } + onDeselect={ () => + setAttributes( { + columns: 3, + } ) + } + isShownByDefault + > + + setAttributes( { columns: value } ) + } + min={ 2 } + max={ 6 } + required + /> + + ) } + + + ) +} + +/** + * The edit function describes the structure of your block in the context of the + * editor. This represents what the editor will render when the block is used. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit + * + * @return {Element} Element to render. + */ +export default function Edit( { attributes, setAttributes } ) { + const { + postsToShow, + order, + orderBy, + categories, + selectedAuthor, + displayFeaturedImage, + displayPostContentRadio, + displayPostContent, + displayPostDate, + displayWebsiteUrl, + postLayout, + columns, + excerptLength, + featuredImageAlign, + featuredImageSizeSlug, + featuredImageSizeWidth, + featuredImageSizeHeight, + addLinkToFeaturedImage, + } = attributes; + console.log(`postLayout: ${postLayout}`); + + const inspectorControls = ( + + + + ); + + const layoutControls = [ + { + icon: list, + title: _x( 'List view', 'Latest posts block display setting' ), + onClick: () => setAttributes( { postLayout: 'list' } ), + isActive: postLayout === 'list', + }, + { + icon: grid, + title: _x( 'Grid view', 'Latest posts block display setting' ), + onClick: () => setAttributes( { postLayout: 'grid' } ), + isActive: postLayout === 'grid', + }, + ]; + + return ( + <> + { inspectorControls } + + + + +
+ +
+ + ); +} diff --git a/src/block/website-list/editor.scss b/src/block/website-list/editor.scss new file mode 100644 index 0000000..da40879 --- /dev/null +++ b/src/block/website-list/editor.scss @@ -0,0 +1,9 @@ +/** + * The following styles get applied inside the editor only. + * + * Replace them with your own styles or remove the file completely. + */ + +.wp-block-webring-website-list { + border: 1px dotted #f00; +} diff --git a/src/block/website-list/index.js b/src/block/website-list/index.js new file mode 100644 index 0000000..d82621b --- /dev/null +++ b/src/block/website-list/index.js @@ -0,0 +1,33 @@ +/** + * Registers a new block provided a unique name and an object defining its behavior. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ + */ +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. + * All files containing `style` keyword are bundled together. The code used + * gets applied both to the front of your site and to the editor. + * + * @see https://www.npmjs.com/package/@wordpress/scripts#using-css + */ +import './style.scss'; + +/** + * Internal dependencies + */ +import Edit from './edit'; +import metadata from './block.json'; + +/** + * Every block starts by registering a new block type definition. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ + */ +registerBlockType( metadata.name, { + /** + * @see ./edit.js + */ + edit: Edit, +} ); diff --git a/src/block/website-list/render.php b/src/block/website-list/render.php new file mode 100644 index 0000000..7b4edfd --- /dev/null +++ b/src/block/website-list/render.php @@ -0,0 +1,46 @@ + 'webring_website', + 'posts_per_page' => -1, + 'orderby' => 'title', + 'order' => 'ASC', + ) +); + +?> +
> + have_posts() ) : ?> + + +

+ +
diff --git a/src/block/website-list/style.scss b/src/block/website-list/style.scss new file mode 100644 index 0000000..ae31d76 --- /dev/null +++ b/src/block/website-list/style.scss @@ -0,0 +1,12 @@ +/** + * The following styles get applied both on the front of your site + * and in the editor. + * + * Replace them with your own styles or remove the file completely. + */ + +.wp-block-webring-website-list { + background-color: #21759b; + color: #fff; + padding: 2px; +} diff --git a/src/block/website-list/view.js b/src/block/website-list/view.js new file mode 100644 index 0000000..f0df523 --- /dev/null +++ b/src/block/website-list/view.js @@ -0,0 +1,22 @@ +/** + * Use this file for JavaScript code that you want to run in the front-end + * on posts/pages that contain this block. + * + * When this file is defined as the value of the `viewScript` property + * in `block.json` it will be enqueued on the front end of the site. + * + * Example: + * + * ```js + * { + * "viewScript": "file:./view.js" + * } + * ``` + * + * If you're not making any changes to this file because your project doesn't need any + * JavaScript running in the front-end, then you should delete this file and remove + * the `viewScript` property from `block.json`. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#view-script + */ + From 3a24966612d33f94842ac5bef094f01ae0227eb5 Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Wed, 18 Mar 2026 23:11:04 +0100 Subject: [PATCH 02/14] Remove count and filter options and add styling options --- src/block/website-list/block.json | 54 ++++++++--- src/block/website-list/edit.js | 50 +++------- src/block/website-list/editor.scss | 19 +++- src/block/website-list/render.php | 151 ++++++++++++++++++++++------- src/block/website-list/style.scss | 94 +++++++++++++++++- 5 files changed, 277 insertions(+), 91 deletions(-) diff --git a/src/block/website-list/block.json b/src/block/website-list/block.json index 3e69289..d039789 100644 --- a/src/block/website-list/block.json +++ b/src/block/website-list/block.json @@ -9,7 +9,47 @@ "description": "Example block scaffolded with Create Block tool.", "example": {}, "supports": { - "html": false + "anchor": true, + "align": true, + "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true, + "__experimentalDefaultControls": { + "radius": true, + "color": true, + "width": true, + "style": true + } + } }, "attributes": { "categories": { @@ -18,10 +58,6 @@ "type": "object" } }, - "postsToShow": { - "type": "number", - "default": 5 - }, "postLayout": { "type": "string", "default": "list" @@ -30,14 +66,6 @@ "type": "number", "default": 3 }, - "order": { - "type": "string", - "default": "desc" - }, - "orderBy": { - "type": "string", - "default": "date" - }, "displayFeaturedImage": { "type": "boolean", "default": false diff --git a/src/block/website-list/edit.js b/src/block/website-list/edit.js index c0f910c..0f8e0ff 100644 --- a/src/block/website-list/edit.js +++ b/src/block/website-list/edit.js @@ -94,9 +94,7 @@ import './editor.scss'; function Controls( { attributes, setAttributes } ) { const { - postsToShow, - order, - orderBy, + categories, selectedAuthor, displayFeaturedImage, @@ -154,9 +152,9 @@ function Controls( { attributes, setAttributes } ) { const selectCategories = ( tokens ) => { const hasNoSuggestion = tokens.some( ( token ) => - typeof token === 'string' && ! categorySuggestions[ token ] + typeof token === 'string' && !categorySuggestions[ token ] ); - if ( hasNoSuggestion ) { + if (hasNoSuggestion) { return; } // Categories that are already will be objects, while new additions will be strings (the name). @@ -168,7 +166,7 @@ function Controls( { attributes, setAttributes } ) { } ); // We do nothing if the category is not selected // from suggestions. - if ( allCategories.includes( null ) ) { + if (allCategories.includes( null )) { return false; } setAttributes( { categories: allCategories } ); @@ -176,7 +174,6 @@ function Controls( { attributes, setAttributes } ) { return ( <> - @@ -186,7 +183,7 @@ function Controls( { attributes, setAttributes } ) { } > !! displayWebsiteUrl } + hasValue={ () => !!displayWebsiteUrl } label={ __( 'Display website URL' ) } onDeselect={ () => setAttributes( { displayWebsiteUrl: false } ) @@ -216,7 +213,7 @@ function Controls( { attributes, setAttributes } ) { } > !! displayFeaturedImage } + hasValue={ () => !!displayFeaturedImage } label={ __( 'Display featured image' ) } onDeselect={ () => setAttributes( { displayFeaturedImage: false } ) @@ -252,11 +249,11 @@ function Controls( { attributes, setAttributes } ) { { const newAttrs = {}; - if ( value.hasOwnProperty( 'width' ) ) { + if (value.hasOwnProperty( 'width' )) { newAttrs.featuredImageSizeWidth = value.width; } - if ( value.hasOwnProperty( 'height' ) ) { + if (value.hasOwnProperty( 'height' )) { newAttrs.featuredImageSizeHeight = value.height; } @@ -281,7 +278,7 @@ function Controls( { attributes, setAttributes } ) { /> !! featuredImageAlign } + hasValue={ () => !!featuredImageAlign } label={ __( 'Image alignment' ) } onDeselect={ () => setAttributes( { @@ -319,7 +316,7 @@ function Controls( { attributes, setAttributes } ) { !! addLinkToFeaturedImage } + hasValue={ () => !!addLinkToFeaturedImage } label={ __( 'Add link to featured image' ) } onDeselect={ () => setAttributes( { @@ -345,9 +342,6 @@ function Controls( { attributes, setAttributes } ) { label={ __( 'Sorting and filtering' ) } resetAll={ () => setAttributes( { - order: 'desc', - orderBy: 'date', - postsToShow: 5, categories: undefined, selectedAuthor: undefined, columns: 3, @@ -356,18 +350,12 @@ function Controls( { attributes, setAttributes } ) { > - order !== 'desc' || - orderBy !== 'date' || - postsToShow !== 5 || categories?.length > 0 || !!selectedAuthor } label={ __( 'Sort and filter' ) } onDeselect={ () => setAttributes( { - order: 'desc', - orderBy: 'date', - postsToShow: 5, categories: undefined, selectedAuthor: undefined, } ) @@ -375,17 +363,6 @@ function Controls( { attributes, setAttributes } ) { isShownByDefault > - setAttributes( { order: value } ) - } - onOrderByChange={ ( value ) => - setAttributes( { orderBy: value } ) - } - onNumberOfItemsChange={ ( value ) => - setAttributes( { postsToShow: value } ) - } categorySuggestions={ categorySuggestions } onCategoryChange={ selectCategories } selectedCategories={ categories } @@ -431,9 +408,6 @@ function Controls( { attributes, setAttributes } ) { */ export default function Edit( { attributes, setAttributes } ) { const { - postsToShow, - order, - orderBy, categories, selectedAuthor, displayFeaturedImage, @@ -450,7 +424,7 @@ export default function Edit( { attributes, setAttributes } ) { featuredImageSizeHeight, addLinkToFeaturedImage, } = attributes; - console.log(`postLayout: ${postLayout}`); + console.log( `postLayout: ${ postLayout }` ); const inspectorControls = ( @@ -483,7 +457,7 @@ export default function Edit( { attributes, setAttributes } ) { -
+
li { + overflow: hidden; + } +} + +.wp-block-webring-website-list li a > div { + display: inline; +} + +:root :where(.wp-block-webring-website-list) { + padding-left: 2.5em; +} +:root { + :where(.wp-block-webring-website-list.is-grid), + :where(.wp-block-webring-website-list__list) { + padding-left: 0; + } } diff --git a/src/block/website-list/render.php b/src/block/website-list/render.php index 7b4edfd..1cc516b 100644 --- a/src/block/website-list/render.php +++ b/src/block/website-list/render.php @@ -4,43 +4,122 @@ * * @package webring * - * The following variables are exposed to the file: + * The following variables are exposed to the file: * - * $attributes (array): The block attributes. - * $content (string): The block default content. - * $block (WP_Block): The block instance. + * @var array $attributes The block attributes. + * @var string $content The block default content. + * @var WP_Block $block The block instance. * - * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render + * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render */ -$query = new WP_Query( - array( - 'post_type' => 'webring_website', - 'posts_per_page' => -1, - 'orderby' => 'title', - 'order' => 'ASC', - ) -); - -?> -
> - have_posts() ) : ?> -
    - have_posts() ) : - $query->the_post(); - ?> -
  • - - - -
  • - -
- -

- -
+$args = [ + 'post_type' => 'webring_website', + 'post_status' => 'publish', +]; + +if ( ! empty( $attributes['categories'] ) ) { + $args['tax_query'] = [ + [ + 'taxonomy' => 'webring_category', + 'field' => 'term_id', + 'terms' => array_column( $attributes['categories'], 'id' ), + ], + ]; +} + +$query = new WP_Query(); +$webring_websites = $query->query( $args ); + +if ( empty( $webring_websites ) ) { + if ( ! wp_is_serving_rest_request() ) { + return; + } + + printf( + '
%s
', + __( 'No websites found. Try to change your filters', 'webring' ), + ); + + return; +} else { + if ( isset( $attributes['displayFeaturedImage'] ) && $attributes['displayFeaturedImage'] ) { + update_post_thumbnail_cache( $query ); + } + + $list_items_markup = ''; + + foreach ( $webring_websites as $post ) { + $post_link = esc_url( get_post_meta( $post, 'webring_website_url' ) ); + $title = get_the_title( $post ); + + if ( ! $title ) { + $title = __( '(no title)' ); + } + + $list_items_markup .= '
  • '; + + if ( $attributes['displayFeaturedImage'] && has_post_thumbnail( $post ) ) { + $image_style = ''; + if ( isset( $attributes['featuredImageSizeWidth'] ) ) { + $image_style .= sprintf( 'max-width:%spx;', $attributes['featuredImageSizeWidth'] ); + } + if ( isset( $attributes['featuredImageSizeHeight'] ) ) { + $image_style .= sprintf( 'max-height:%spx;', $attributes['featuredImageSizeHeight'] ); + } + + $image_classes = 'wp-block-webring-website-list__featured-image'; + if ( isset( $attributes['featuredImageAlign'] ) ) { + $image_classes .= ' align' . $attributes['featuredImageAlign']; + } + + $featured_image = get_the_post_thumbnail( + $post, + $attributes['featuredImageSizeSlug'], + [ + 'style' => esc_attr( $image_style ), + ] + ); + if ( $attributes['addLinkToFeaturedImage'] ) { + $featured_image = sprintf( + '%3$s', + esc_url( $post_link ), + esc_attr( $title ), + $featured_image + ); + } + $list_items_markup .= sprintf( + '
    %2$s
    ', + esc_attr( $image_classes ), + $featured_image + ); + } + + $list_items_markup .= sprintf( + '%2$s', + esc_url( $post_link ), + $title + ); + + $list_items_markup .= "
  • \n"; + } + + $classes = [ 'wp-block-webring-website-list__list' ]; + if ( isset( $attributes['postLayout'] ) && 'grid' === $attributes['postLayout'] ) { + $classes[] = 'is-grid'; + } + if ( isset( $attributes['columns'] ) && 'grid' === $attributes['postLayout'] ) { + $classes[] = 'columns-' . $attributes['columns']; + } + if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) { + $classes[] = 'has-link-color'; + } + + $wrapper_attributes = get_block_wrapper_attributes( [ 'class' => implode( ' ', $classes ) ] ); + + printf( + '
      %2$s
    ', + $wrapper_attributes, + $list_items_markup + ); +} diff --git a/src/block/website-list/style.scss b/src/block/website-list/style.scss index ae31d76..d407dee 100644 --- a/src/block/website-list/style.scss +++ b/src/block/website-list/style.scss @@ -5,8 +5,96 @@ * Replace them with your own styles or remove the file completely. */ +@use "@wordpress/base-styles/mixins" as *; + .wp-block-webring-website-list { - background-color: #21759b; - color: #fff; - padding: 2px; + // This block has customizable padding, border-box makes that more predictable. + box-sizing: border-box; + + &.alignleft { + /*rtl:ignore*/ + margin-right: 2em; + } + &.alignright { + /*rtl:ignore*/ + margin-left: 2em; + } + &.wp-block-webring-website-list__list { + list-style: none; + + li { + clear: both; + overflow-wrap: break-word; + } + } + + &.is-grid { + display: flex; + flex-wrap: wrap; + + li { + margin: 0 1.25em 1.25em 0; + width: 100%; + } + } + + @include break-small { + @for $i from 2 through 6 { + &.columns-#{ $i } li { + width: calc((100% / #{$i}) - 1.25em + (1.25em / #{$i})); + + &:nth-child(#{ $i }n) { + margin-right: 0; + } + } + } + } +} + +:root { + :where(.wp-block-webring-website-list.is-grid) { + padding: 0; + } + :where(.wp-block-webring-website-list.wp-block-webring-website-list__list) { + padding-left: 0; + } +} + +.wp-block-webring-website-list__post-date, +.wp-block-webring-website-list__post-author { + display: block; + font-size: 0.8125em; +} + +.wp-block-webring-website-list__post-excerpt, +.wp-block-webring-website-list__post-full-content { + margin-top: 0.5em; + margin-bottom: 1em; +} + +.wp-block-webring-website-list__featured-image { + a { + display: inline-block; + } + img { + height: auto; + width: auto; + max-width: 100%; + } + &.alignleft { + /*rtl:ignore*/ + margin-right: 1em; + /*rtl:ignore*/ + float: left; + } + &.alignright { + /*rtl:ignore*/ + margin-left: 1em; + /*rtl:ignore*/ + float: right; + } + &.aligncenter { + margin-bottom: 1em; + text-align: center; + } } From ff6c5c4b3f419ff5e18144ab8d1bc634dc70a508 Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Fri, 27 Mar 2026 17:23:11 +0100 Subject: [PATCH 03/14] Fixing styling in the Block Editor --- src/block/website-list/editor.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/block/website-list/editor.scss b/src/block/website-list/editor.scss index 5dd8a89..10dde32 100644 --- a/src/block/website-list/editor.scss +++ b/src/block/website-list/editor.scss @@ -3,6 +3,9 @@ * * Replace them with your own styles or remove the file completely. */ +div.wp-block-webring-website-list { + padding: 0; +} .wp-block-webring-website-list { // Apply overflow for post items, so any floated featured images won't crop the focus style. From ada0a7277cc1684547d1cbf1c3e6ee6b62f6cb7f Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Sat, 28 Mar 2026 15:32:54 +0100 Subject: [PATCH 04/14] Add thumbnail support to the website CPT --- lib/PostType/Website.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/PostType/Website.php b/lib/PostType/Website.php index afd9473..82579a1 100644 --- a/lib/PostType/Website.php +++ b/lib/PostType/Website.php @@ -63,9 +63,10 @@ public function register_post_type() { 'show_ui' => true, 'show_in_nav_menus' => true, 'supports' => [ + 'custom-fields', 'editor', + 'thumbnail', 'title', - 'custom-fields', ], 'has_archive' => true, 'rewrite' => true, From 97e0c8ee71855e429528a29abd7996a8871f4c17 Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Sat, 28 Mar 2026 15:40:48 +0100 Subject: [PATCH 05/14] Unset default width/height thumbnail sizes --- src/block/website-list/block.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/block/website-list/block.json b/src/block/website-list/block.json index d039789..712a0ab 100644 --- a/src/block/website-list/block.json +++ b/src/block/website-list/block.json @@ -79,12 +79,10 @@ "default": "thumbnail" }, "featuredImageSizeWidth": { - "type": "number", - "default": 150 + "type": "number" }, "featuredImageSizeHeight": { - "type": "number", - "default": 150 + "type": "number" }, "addLinkToFeaturedImage": { "type": "boolean", From 425fa312eaf70f0807ed6be88c2d27393bf486aa Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Sat, 28 Mar 2026 15:46:15 +0100 Subject: [PATCH 06/14] Remove unused view.js file --- src/block/website-list/block.json | 3 +-- src/block/website-list/view.js | 22 ---------------------- 2 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 src/block/website-list/view.js diff --git a/src/block/website-list/block.json b/src/block/website-list/block.json index 712a0ab..f659f87 100644 --- a/src/block/website-list/block.json +++ b/src/block/website-list/block.json @@ -93,6 +93,5 @@ "editorScript": "file:./index.js", "editorStyle": "file:./index.css", "style": "file:./style-index.css", - "render": "file:./render.php", - "viewScript": "file:./view.js" + "render": "file:./render.php" } diff --git a/src/block/website-list/view.js b/src/block/website-list/view.js deleted file mode 100644 index f0df523..0000000 --- a/src/block/website-list/view.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Use this file for JavaScript code that you want to run in the front-end - * on posts/pages that contain this block. - * - * When this file is defined as the value of the `viewScript` property - * in `block.json` it will be enqueued on the front end of the site. - * - * Example: - * - * ```js - * { - * "viewScript": "file:./view.js" - * } - * ``` - * - * If you're not making any changes to this file because your project doesn't need any - * JavaScript running in the front-end, then you should delete this file and remove - * the `viewScript` property from `block.json`. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#view-script - */ - From 4cb05d8c0bcd10c5561d3c9e544fb1d5e988b350 Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Sat, 28 Mar 2026 15:49:03 +0100 Subject: [PATCH 07/14] Remove unused imports and properties --- src/block/website-list/edit.js | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/src/block/website-list/edit.js b/src/block/website-list/edit.js index 0f8e0ff..cb398a8 100644 --- a/src/block/website-list/edit.js +++ b/src/block/website-list/edit.js @@ -2,11 +2,8 @@ * WordPress dependencies */ import { - Placeholder, QueryControls, - RadioControl, RangeControl, - Spinner, ToggleControl, ToolbarGroup, __experimentalToggleGroupControl as ToggleGroupControl, @@ -14,16 +11,14 @@ import { __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; -import { dateI18n, format, getSettings } from '@wordpress/date'; import { InspectorControls, BlockControls, __experimentalImageSizeControl as ImageSizeControl, store as blockEditorStore, } from '@wordpress/block-editor'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { - pin, list, grid, alignNone, @@ -32,9 +27,6 @@ import { positionRight, } from '@wordpress/icons'; import { store as coreStore } from '@wordpress/core-data'; -import { store as noticeStore } from '@wordpress/notices'; -import { useInstanceId } from '@wordpress/compose'; -import { createInterpolateElement } from '@wordpress/element'; import ServerSideRender from '@wordpress/server-side-render'; /** @@ -74,7 +66,7 @@ const imageAlignmentOptions = [ * * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/ */ -import { __, _x, sprintf } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; /** * React hook that is used to mark the block wrapper element. @@ -94,7 +86,6 @@ import './editor.scss'; function Controls( { attributes, setAttributes } ) { const { - categories, selectedAuthor, displayFeaturedImage, @@ -158,14 +149,13 @@ function Controls( { attributes, setAttributes } ) { return; } // Categories that are already will be objects, while new additions will be strings (the name). - // allCategories nomalizes the array so that they are all objects. + // allCategories normalize the array so that they are all objects. const allCategories = tokens.map( ( token ) => { return typeof token === 'string' ? categorySuggestions[ token ] : token; } ); - // We do nothing if the category is not selected - // from suggestions. + // We do nothing if the category is not selected from suggestions. if (allCategories.includes( null )) { return false; } @@ -408,21 +398,7 @@ function Controls( { attributes, setAttributes } ) { */ export default function Edit( { attributes, setAttributes } ) { const { - categories, - selectedAuthor, - displayFeaturedImage, - displayPostContentRadio, - displayPostContent, - displayPostDate, - displayWebsiteUrl, postLayout, - columns, - excerptLength, - featuredImageAlign, - featuredImageSizeSlug, - featuredImageSizeWidth, - featuredImageSizeHeight, - addLinkToFeaturedImage, } = attributes; console.log( `postLayout: ${ postLayout }` ); From 75ee2cf15bb33cc875191090e1922d814f2ec918 Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Sat, 28 Mar 2026 18:44:04 +0100 Subject: [PATCH 08/14] Ignore `missing_direct_file_access_protection`, since it has false positives Ignore `missing_composer_json_file`, since it has false positives --- .github/workflows/wordpress-plugin-check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wordpress-plugin-check.yml b/.github/workflows/wordpress-plugin-check.yml index 877b81d..19b2e08 100644 --- a/.github/workflows/wordpress-plugin-check.yml +++ b/.github/workflows/wordpress-plugin-check.yml @@ -33,4 +33,6 @@ jobs: - name: Run plugin check uses: wordpress/plugin-check-action@v1 with: + ignore-codes: | + missing_direct_file_access_protection build-dir: ./tmp-build/${{ github.event.repository.name }} From 1ef240e5f25146bbccfaa94478d5ebc8b1280237 Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Sat, 28 Mar 2026 18:16:40 +0100 Subject: [PATCH 09/14] Add readme.txt file --- readme.txt | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 readme.txt diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..ea69301 --- /dev/null +++ b/readme.txt @@ -0,0 +1,28 @@ +=== Webring === + +Contributors: Kau-Boy +Stable tag: 1.0.0 +Tested up to: 6.9 +Requires at least: 6.8 +Requires PHP: 7.4 +License: GPLv3 +License URI: https://www.gnu.org/licenses/gpl-3.0.txt +Tags: webring, fediverse, open web, blogroll + +Create and manage webrings on your WordPress site. + +== Description == + +Create and manage webrings on your WordPress site with a dedicated +custom post type for member websites. You can optionally organize +entries with categories to keep your webrings structured and easy +to maintain. + +The plugin also provides a block for displaying the HTML snippet +visitors can use to join or link to your webrings, plus a block +that lists all websites currently included in a webring. + +== Changelog == + += 1.0.0 = +* First stable version From 8498793b3e11a4240b8ae3519735225c1585fde6 Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Sat, 28 Mar 2026 18:19:12 +0100 Subject: [PATCH 10/14] Fix some PHPCS and PSP issues --- src/block/html-snippet/render.php | 2 ++ src/block/website-list/render.php | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/block/html-snippet/render.php b/src/block/html-snippet/render.php index ac80134..eabb9bb 100644 --- a/src/block/html-snippet/render.php +++ b/src/block/html-snippet/render.php @@ -13,6 +13,8 @@ * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render */ +// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + $webring_name = $attributes['webringName'] ?? 'webring'; $show_copy_instructions = $attributes['showCopyInstructions'] ?? true; $show_copy_button = $attributes['showCopyButton'] ?? true; diff --git a/src/block/website-list/render.php b/src/block/website-list/render.php index 1cc516b..83e4b86 100644 --- a/src/block/website-list/render.php +++ b/src/block/website-list/render.php @@ -13,6 +13,8 @@ * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render */ +// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + $args = [ 'post_type' => 'webring_website', 'post_status' => 'publish', @@ -38,7 +40,7 @@ printf( '
    %s
    ', - __( 'No websites found. Try to change your filters', 'webring' ), + esc_html__( 'No websites found. Try to change your filters', 'webring' ), ); return; @@ -49,17 +51,17 @@ $list_items_markup = ''; - foreach ( $webring_websites as $post ) { - $post_link = esc_url( get_post_meta( $post, 'webring_website_url' ) ); - $title = get_the_title( $post ); + foreach ( $webring_websites as $website ) { + $website_link = get_post_meta( $website, 'webring_website_url' ); + $website_title = get_the_title( $website ); - if ( ! $title ) { - $title = __( '(no title)' ); + if ( ! $website_title ) { + $website_title = __( '(no title)', 'webring' ); } $list_items_markup .= '
  • '; - if ( $attributes['displayFeaturedImage'] && has_post_thumbnail( $post ) ) { + if ( $attributes['displayFeaturedImage'] && has_post_thumbnail( $website ) ) { $image_style = ''; if ( isset( $attributes['featuredImageSizeWidth'] ) ) { $image_style .= sprintf( 'max-width:%spx;', $attributes['featuredImageSizeWidth'] ); @@ -74,7 +76,7 @@ } $featured_image = get_the_post_thumbnail( - $post, + $website, $attributes['featuredImageSizeSlug'], [ 'style' => esc_attr( $image_style ), @@ -83,8 +85,8 @@ if ( $attributes['addLinkToFeaturedImage'] ) { $featured_image = sprintf( '%3$s', - esc_url( $post_link ), - esc_attr( $title ), + esc_url( $website_link ), + esc_attr( $website_title ), $featured_image ); } @@ -97,8 +99,8 @@ $list_items_markup .= sprintf( '%2$s', - esc_url( $post_link ), - $title + esc_url( $website_link ), + $website_title ); $list_items_markup .= "
  • \n"; @@ -117,6 +119,7 @@ $wrapper_attributes = get_block_wrapper_attributes( [ 'class' => implode( ' ', $classes ) ] ); + // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped printf( '
      %2$s
    ', $wrapper_attributes, From 99c0fda773c264035931104a8abababa3c52b790 Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Sat, 28 Mar 2026 18:25:20 +0100 Subject: [PATCH 11/14] Update to the latest WP-CLI version --- .github/workflows/wordpress-plugin-check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wordpress-plugin-check.yml b/.github/workflows/wordpress-plugin-check.yml index 19b2e08..e552572 100644 --- a/.github/workflows/wordpress-plugin-check.yml +++ b/.github/workflows/wordpress-plugin-check.yml @@ -19,10 +19,10 @@ jobs: with: php-version: latest coverage: none - tools: wp-cli + tools: wp-cli:v2.12 - name: Install latest version of dist-archive-command - run: wp package install wp-cli/dist-archive-command:@stable + run: wp package install wp-cli/dist-archive-command:v3.1.0 - name: Build plugin run: | From f2393c21db802fa036116cd56fc04be692100b6f Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Sat, 28 Mar 2026 18:54:50 +0100 Subject: [PATCH 12/14] Ignore some folders on dist-archive --- .distignore | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .distignore diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..590accc --- /dev/null +++ b/.distignore @@ -0,0 +1,16 @@ +*.lock +.distignore +.editorconfig +.eslintrc +.git +.gitattributes +.github +.gitignore +.stylelintrc.json +.wordpress-org +composer.json +composer.lock +node_modules +package-lock.json +package.json +phpcs.xml From f3fd71a8b85e8ad011f7dfb1b4ffa9d579d5df2a Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Sat, 28 Mar 2026 19:15:36 +0100 Subject: [PATCH 13/14] Install no-dev dependencies only --- .github/workflows/wordpress-plugin-check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/wordpress-plugin-check.yml b/.github/workflows/wordpress-plugin-check.yml index e552572..515d98a 100644 --- a/.github/workflows/wordpress-plugin-check.yml +++ b/.github/workflows/wordpress-plugin-check.yml @@ -21,6 +21,9 @@ jobs: coverage: none tools: wp-cli:v2.12 + - name: Install no-dev dependencies only + run: composer install --no-dev -o + - name: Install latest version of dist-archive-command run: wp package install wp-cli/dist-archive-command:v3.1.0 From 2954ed2ba521314cea7b58828869b5033c2dcdbc Mon Sep 17 00:00:00 2001 From: Bernhard Kau Date: Sat, 28 Mar 2026 20:55:56 +0100 Subject: [PATCH 14/14] Include composer.json in dist archive --- .distignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.distignore b/.distignore index 590accc..fa1b908 100644 --- a/.distignore +++ b/.distignore @@ -8,8 +8,6 @@ .gitignore .stylelintrc.json .wordpress-org -composer.json -composer.lock node_modules package-lock.json package.json