diff --git a/.distignore b/.distignore
new file mode 100644
index 0000000..fa1b908
--- /dev/null
+++ b/.distignore
@@ -0,0 +1,14 @@
+*.lock
+.distignore
+.editorconfig
+.eslintrc
+.git
+.gitattributes
+.github
+.gitignore
+.stylelintrc.json
+.wordpress-org
+node_modules
+package-lock.json
+package.json
+phpcs.xml
diff --git a/.github/workflows/wordpress-plugin-check.yml b/.github/workflows/wordpress-plugin-check.yml
index 877b81d..515d98a 100644
--- a/.github/workflows/wordpress-plugin-check.yml
+++ b/.github/workflows/wordpress-plugin-check.yml
@@ -19,10 +19,13 @@ jobs:
with:
php-version: latest
coverage: none
- tools: wp-cli
+ 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:@stable
+ run: wp package install wp-cli/dist-archive-command:v3.1.0
- name: Build plugin
run: |
@@ -33,4 +36,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 }}
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,
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/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
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/block.json b/src/block/website-list/block.json
new file mode 100644
index 0000000..f659f87
--- /dev/null
+++ b/src/block/website-list/block.json
@@ -0,0 +1,97 @@
+{
+ "$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": {
+ "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": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ },
+ "postLayout": {
+ "type": "string",
+ "default": "list"
+ },
+ "columns": {
+ "type": "number",
+ "default": 3
+ },
+ "displayFeaturedImage": {
+ "type": "boolean",
+ "default": false
+ },
+ "featuredImageAlign": {
+ "type": "string",
+ "enum": [ "left", "center", "right" ]
+ },
+ "featuredImageSizeSlug": {
+ "type": "string",
+ "default": "thumbnail"
+ },
+ "featuredImageSizeWidth": {
+ "type": "number"
+ },
+ "featuredImageSizeHeight": {
+ "type": "number"
+ },
+ "addLinkToFeaturedImage": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ "textdomain": "webring",
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css",
+ "render": "file:./render.php"
+}
diff --git a/src/block/website-list/edit.js b/src/block/website-list/edit.js
new file mode 100644
index 0000000..cb398a8
--- /dev/null
+++ b/src/block/website-list/edit.js
@@ -0,0 +1,444 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ QueryControls,
+ RangeControl,
+ ToggleControl,
+ ToolbarGroup,
+ __experimentalToggleGroupControl as ToggleGroupControl,
+ __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon,
+ __experimentalToolsPanel as ToolsPanel,
+ __experimentalToolsPanelItem as ToolsPanelItem,
+} from '@wordpress/components';
+import {
+ InspectorControls,
+ BlockControls,
+ __experimentalImageSizeControl as ImageSizeControl,
+ store as blockEditorStore,
+} from '@wordpress/block-editor';
+import { useSelect } from '@wordpress/data';
+import {
+ list,
+ grid,
+ alignNone,
+ positionLeft,
+ positionCenter,
+ positionRight,
+} from '@wordpress/icons';
+import { store as coreStore } from '@wordpress/core-data';
+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 } 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 {
+ 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 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.
+ 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( {
+ categories: undefined,
+ selectedAuthor: undefined,
+ columns: 3,
+ } )
+ }
+ >
+
+ categories?.length > 0 ||
+ !!selectedAuthor
+ }
+ label={ __( 'Sort and filter' ) }
+ onDeselect={ () =>
+ setAttributes( {
+ categories: undefined,
+ selectedAuthor: undefined,
+ } )
+ }
+ isShownByDefault
+ >
+
+
+
+ { 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 {
+ postLayout,
+ } = 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..10dde32
--- /dev/null
+++ b/src/block/website-list/editor.scss
@@ -0,0 +1,29 @@
+/**
+ * The following styles get applied inside the editor only.
+ *
+ * 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.
+ > 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/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..83e4b86
--- /dev/null
+++ b/src/block/website-list/render.php
@@ -0,0 +1,128 @@
+ '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(
+ '',
+ esc_html__( '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 $website ) {
+ $website_link = get_post_meta( $website, 'webring_website_url' );
+ $website_title = get_the_title( $website );
+
+ if ( ! $website_title ) {
+ $website_title = __( '(no title)', 'webring' );
+ }
+
+ $list_items_markup .= '';
+
+ if ( $attributes['displayFeaturedImage'] && has_post_thumbnail( $website ) ) {
+ $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(
+ $website,
+ $attributes['featuredImageSizeSlug'],
+ [
+ 'style' => esc_attr( $image_style ),
+ ]
+ );
+ if ( $attributes['addLinkToFeaturedImage'] ) {
+ $featured_image = sprintf(
+ '%3$s',
+ esc_url( $website_link ),
+ esc_attr( $website_title ),
+ $featured_image
+ );
+ }
+ $list_items_markup .= sprintf(
+ '%2$s
',
+ esc_attr( $image_classes ),
+ $featured_image
+ );
+ }
+
+ $list_items_markup .= sprintf(
+ '%2$s',
+ esc_url( $website_link ),
+ $website_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 ) ] );
+
+ // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
+ printf(
+ '',
+ $wrapper_attributes,
+ $list_items_markup
+ );
+}
diff --git a/src/block/website-list/style.scss b/src/block/website-list/style.scss
new file mode 100644
index 0000000..d407dee
--- /dev/null
+++ b/src/block/website-list/style.scss
@@ -0,0 +1,100 @@
+/**
+ * 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.
+ */
+
+@use "@wordpress/base-styles/mixins" as *;
+
+.wp-block-webring-website-list {
+ // 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;
+ }
+}