Newsletters? Spam protection? Image galleries? We have you covered, no plugin needed.
WORDPRESS
Making a WordPress plugin extensible with PHP classes
WordPress plugins can be extended with additional functionality, as demonstrated by popular plugins like WooCommerce and Gravity Forms. In the article “Architecting a WordPress plugin to support extensions,” we learn there are two primary ways to make a WordPress plugin extensible:
- By setting up hooks (actions and filters) for extension plugins to inject their own functionality
- By providing PHP classes that extension plugins can inherit
The first method relies more on documentation, detailing available hooks and their usage. The second method, by contrast, offers ready-to-use code for extensions, reducing the need for extensive documentation. This is advantageous because creating documentation alongside code can complicate the plugin’s management and release.
Providing PHP classes directly effectively replaces documentation with code. Instead of teaching how to implement a feature, the plugin supplies the necessary PHP code, simplifying the task for third-party developers.
Let’s explore some techniques for achieving this, with the ultimate goal of fostering an ecosystem of integrations around our WordPress plugin.
Defining base PHP classes in the WordPress plugin
The WordPress plugin will include PHP classes intended for use by extension plugins. These PHP classes might not be used by the main plugin itself but are provided specifically for others to use.
Let’s see how this is implemented in the open-source Gato GraphQL plugin.
AbstractPlugin class:
AbstractPlugin
represents a plugin, both for the main Gato GraphQL plugin and its extensions:
abstract class AbstractPlugin implements PluginInterface
{
protected string $pluginBaseName;
protected string $pluginSlug;
protected string $pluginName;
public function __construct(
protected string $pluginFile,
protected string $pluginVersion,
?string $pluginName,
) {
$this->pluginBaseName = plugin_basename($pluginFile);
$this->pluginSlug = dirname($this->pluginBaseName);
$this->pluginName = $pluginName ?? $this->pluginBaseName;
}
public function getPluginName(): string
{
return $this->pluginName;
}
public function getPluginBaseName(): string
{
return $this->pluginBaseName;
}
public function getPluginSlug(): string
{
return $this->pluginSlug;
}
public function getPluginFile(): string
{
return $this->pluginFile;
}
public function getPluginVersion(): string
{
return $this->pluginVersion;
}
public function getPluginDir(): string
{
return dirname($this->pluginFile);
}
public function getPluginURL(): string
{
return plugin_dir_url($this->pluginFile);
}
// ...
}
AbstractMainPlugin class:
AbstractMainPlugin
extends AbstractPlugin
to represent the main plugin:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface
{
public function __construct(
string $pluginFile,
string $pluginVersion,
?string $pluginName,
protected MainPluginInitializationConfigurationInterface $pluginInitializationConfiguration,
) {
parent::__construct(
$pluginFile,
$pluginVersion,
$pluginName,
);
}
// ...
}
AbstractExtension class:
Similarly, AbstractExtension
extends AbstractPlugin
to represent an extension plugin:
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface
{
public function __construct(
string $pluginFile,
string $pluginVersion,
?string $pluginName,
protected ?ExtensionInitializationConfigurationInterface $extensionInitializationConfiguration,
) {
parent::__construct(
$pluginFile,
$pluginVersion,
$pluginName,
);
}
// ...
}
Notice that AbstractExtension
is included within the main plugin, providing functionality to register and initialize an extension. However, it is only used by extensions, not by the main plugin itself.
The AbstractPlugin
class contains shared initialization code invoked at different times. These methods are defined at the ancestor level but are invoked by the inheriting classes according to their lifecycles.
The main plugin and extensions are initialized by executing the setup
method on the corresponding class, invoked from within the main WordPress plugin file.
For instance, in Gato GraphQL, this is done in gatographql.php
:
$pluginFile = __FILE__;
$pluginVersion = '2.4.0';
$pluginName = __('Gato GraphQL', 'gatographql');
PluginApp::getMainPluginManager()->register(new Plugin(
$pluginFile,
$pluginVersion,
$pluginName
))->setup();
setup method:
At the ancestor level, setup
contains the common logic between the plugin and its extensions, such as unregistering them when the plugin is deactivated. This method is not final; It can be overridden by the inheriting classes to add their functionality:
abstract class AbstractPlugin implements PluginInterface
{
// ...
public function setup(): void
{
register_deactivation_hook(
$this->getPluginFile(),
$this->deactivate(...)
);
}
public function deactivate(): void
{
$this->removePluginVersion();
}
private function removePluginVersion(): void
{
$pluginVersions = get_option('gatographql-plugin-versions', []);
unset($pluginVersions[$this->pluginBaseName]);
update_option('gatographql-plugin-versions', $pluginVersions);
}
}
Main plugin’s setup method:
The main plugin’s setup
method initializes the application’s lifecycle. It executes the main plugin’s functionality through methods like initialize
, configureComponents
, configure
, and boot
, and triggers corresponding action hooks for extensions:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface
{
public function setup(): void
{
parent::setup();
add_action('plugins_loaded', function (): void
{
// 1. Initialize main plugin
$this->initialize();
// 2. Initialize extensions
do_action('gatographql:initializeExtension');
// 3. Configure main plugin components
$this->configureComponents();
// 4. Configure extension components
do_action('gatographql:configureExtensionComponents');
// 5. Configure main plugin
$this->configure();
// 6. Configure extension
do_action('gatographql:configureExtension');
// 7. Boot main plugin
$this->boot();
// 8. Boot extension
do_action('gatographql:bootExtension');
}
// ...
}
// ...
}
Extension setup method:
The AbstractExtension
class executes its logic on the corresponding hooks:
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface
{
// ...
final public function setup(): void
{
parent::setup();
add_action('plugins_loaded', function (): void
{
// 2. Initialize extensions
add_action(
'gatographql:initializeExtension',
$this->initialize(...)
);
// 4. Configure extension components
add_action(
'gatographql:configureExtensionComponents',
$this->configureComponents(...)
);
// 6. Configure extension
add_action(
'gatographql:configureExtension',
$this->configure(...)
);
// 8. Boot extension
add_action(
'gatographql:bootExtension',
$this->boot(...)
);
}, 20);
}
}
Methods initialize
, configureComponents
, configure
, and boot
are common to both the main plugin and extensions and may share logic. This shared logic is stored in the AbstractPlugin
class.
For example, the configure
method configures the plugin or extensions, calling callPluginInitializationConfiguration
, which has different implementations for the main plugin and extensions and is defined as abstract and getModuleClassConfiguration
, which provides a default behavior but can be overridden if needed:
abstract class AbstractPlugin implements PluginInterface
{
// ...
public function configure(): void
{
$this->callPluginInitializationConfiguration();
$appLoader = App::getAppLoader();
$appLoader->addModuleClassConfiguration($this->getModuleClassConfiguration());
}
abstract protected function callPluginInitializationConfiguration(): void;
/**
* @return array,mixed> [key]: Module class, [value]: Configuration
*/
public function getModuleClassConfiguration(): array
{
return [];
}
}
The main plugin provides its implementation for callPluginInitializationConfiguration
:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface
{
// ...
protected function callPluginInitializationConfiguration(): void
{
$this->pluginInitializationConfiguration->initialize();
}
}
Similarly, the extension class provides its implementation:
abstract class AbstractExtension extends AbstractPlugin implements ExtensionInterface
{
// ...
protected function callPluginInitializationConfiguration(): void
{
$this->extensionInitializationConfiguration?->initialize();
}
}
Methods initialize
, configureComponents
and boot
are defined at the ancestor level and can be overridden by inheriting classes:
abstract class AbstractPlugin implements PluginInterface
{
// ...
public function initialize(): void
{
$moduleClasses = $this->getModuleClassesToInitialize();
App::getAppLoader()->addModuleClassesToInitialize($moduleClasses);
}
/**
* @return array> List of `Module` class to initialize
*/
abstract protected function getModuleClassesToInitialize(): array;
public function configureComponents(): void
{
$classNamespace = ClassHelpers::getClassPSR4Namespace(get_called_class());
$moduleClass = $classNamespace . '\Module';
App::getModule($moduleClass)->setPluginFolder(dirname($this->pluginFile));
}
public function boot(): void
{
// By default, do nothing
}
}
All methods can be overridden by AbstractMainPlugin
or AbstractExtension
to extend them with their custom functionality.
For the main plugin, the setup
method also removes any caching from the WordPress instance when the plugin or any of its extensions is activated or deactivated:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface
{
public function setup(): void
{
parent::setup();
// ...
// Main-plugin specific methods
add_action(
'activate_plugin',
function (string $pluginFile): void {
$this->maybeRegenerateContainerWhenPluginActivatedOrDeactivated($pluginFile);
}
);
add_action(
'deactivate_plugin',
function (string $pluginFile): void {
$this->maybeRegenerateContainerWhenPluginActivatedOrDeactivated($pluginFile);
}
);
}
public function maybeRegenerateContainerWhenPluginActivatedOrDeactivated(string $pluginFile): void
{
// Removed code for simplicity
}
// ...
}
Similarly, the deactivate
method removes caching and boot
executes additional action hooks for the main plugin only:
abstract class AbstractMainPlugin extends AbstractPlugin implements MainPluginInterface
{
public function deactivate(): void
{
parent::deactivate();
$this->removeTimestamps();
}
protected function removeTimestamps(): void
{
$userSettingsManager = UserSettingsManagerFacade::getInstance();
$userSettingsManager->removeTimestamps();
}
public function boot(): void
{
parent::boot();
add_filter(
'admin_body_class',
function (string $classes): string {
$extensions = PluginApp::getExtensionManager()->getExtensions();
$commercialExtensionActivatedLicenseObjectProperties = SettingsHelpers::getCommercialExtensionActivatedLicenseObjectProperties();
foreach ($extensions as $extension) {
$extensionCommercialExtensionActivatedLicenseObjectProperties = $commercialExtensionActivatedLicenseObjectProperties[$extension->getPluginSlug()] ?? null;
if ($extensionCommercialExtensionActivatedLicenseObjectProperties === null) {
continue;
}
return $classes . ' is-gatographql-customer';
}
return $classes;
}
);
}
}
From all the code presented above, it is clear that when designing and coding a WordPress plugin, we need to consider the needs of its extensions and reuse code across them as much as possible. Implementing sound Object-Oriented Programming patterns (such as the SOLID principles) helps achieve this, making the codebase maintainable for the long term.
Declaring and validating the version dependency
Since the extension inherits from a PHP class provided by the plugin, it’s crucial to validate that the required version of the plugin is present. Failing to do so could cause conflicts that bring the site down.
For example, if the AbstractExtension
class is updated with breaking changes and releases a major version 4.0.0
from the previous 3.4.0
, loading the extension without checking the version could result in a PHP error, preventing WordPress from loading.
To avoid this, the extension must validate that the installed plugin is version 3.x.x
. When version 4.0.0
is installed, the extension will be disabled, thus preventing errors.
The extension can accomplish this validation using the following logic, executed on the plugins_loaded
hook (since the main plugin will be loaded by then) in the extension’s main plugin file. This logic accesses the ExtensionManager
class, which is included in the main plugin to manage extensions:
/**
* Create and set-up the extension
*/
add_action(
'plugins_loaded',
function (): void {
/**
* Extension's name and version.
*
* Use a stability suffix as supported by Composer.
*/
$extensionVersion = '1.1.0';
$extensionName = __('Gato GraphQL - Extension Template');
/**
* The minimum version required from the Gato GraphQL plugin
* to activate the extension.
*/
$gatoGraphQLPluginVersionConstraint="^1.0";
/**
* Validate Gato GraphQL is active
*/
if (!class_exists(GatoGraphQLGatoGraphQLPlugin::class)) {
add_action('admin_notices', function () use ($extensionName) {
printf(
'',
sprintf(
__('Plugin %s is not installed or activated. Without it, plugin %s will not be loaded.'),
__('Gato GraphQL'),
$extensionName
)
);
});
return;
}
$extensionManager = GatoGraphQLGatoGraphQLPluginApp::getExtensionManager();
if (!$extensionManager->assertIsValid(
GatoGraphQLExtension::class,
$extensionVersion,
$extensionName,
$gatoGraphQLPluginVersionConstraint
)) {
return;
}
// Load Composer’s autoloader
require_once(__DIR__ . '/vendor/autoload.php');
// Create and set-up the extension instance
$extensionManager->register(new GatoGraphQLExtension(
__FILE__,
$extensionVersion,
$extensionName,
))->setup();
}
);
Notice how the extension declares a dependency on version constraint ^1.0
of the main plugin (using Composer’s version constraints). Thus, when version 2.0.0
of Gato GraphQL is installed, the extension will not be activated.
The version constraint is validated via the ExtensionManager::assertIsValid
method, which calls Semver::satisfies
(provided by the composer/semver
package):
use ComposerSemverSemver;
class ExtensionManager extends AbstractPluginManager
{
/**
* Validate that the required version of the Gato GraphQL for WP plugin is installed.
*
* If the assertion fails, it prints an error on the WP admin and returns false
*
* @param string|null $mainPluginVersionConstraint the semver version constraint required for the plugin (eg: "^1.0" means >=1.0.0 and getPlugin();
$mainPluginVersion = $mainPlugin->getPluginVersion();
if (
$mainPluginVersionConstraint !== null && !Semver::satisfies(
$mainPluginVersion,
$mainPluginVersionConstraint
)
) {
$this->printAdminNoticeErrorMessage(
sprintf(
__('Extension or bundle %s requires plugin %s to satisfy version constraint %s
, but the current version %s
does not. The extension or bundle has not been loaded.', 'gatographql'),
$extensionName ?? $extensionClass,
$mainPlugin->getPluginName(),
$mainPluginVersionConstraint,
$mainPlugin->getPluginVersion(),
)
);
return false;
}
return true;
}
protected function printAdminNoticeErrorMessage(string $errorMessage): void
{
add_action('admin_notices', function () use ($errorMessage): void {
$adminNotice_safe = sprintf(
'',
$errorMessage
);
echo $adminNotice_safe;
});
}
}
Running integration tests against a WordPress server
To make it easier for third-party developers to create extensions for your plugins, provide them with tools for development and testing, including workflows for their continuous integration and continuous delivery (CI/CD) processes.
During development, anyone can easily spin up a web server using DevKinsta, install the plugin for which they are coding the extension, and immediately validate that the extension is compatible with the plugin.
To automate testing during CI/CD, we need to have the web server accessible over a network to the CI/CD service. Services such as InstaWP can create a sandbox site with WordPress installed for this purpose.
If the extension’s codebase is hosted on GitHub, developers can use GitHub Actions to run integration tests against the InstaWP service. The following workflow installs the extension on an InstaWP sandbox site (alongside the latest stable version of the main plugin) and then runs the integration tests:
name: Integration tests (InstaWP)
on:
workflow_run:
workflows: [Generate plugins]
types:
- completed
jobs:
provide_data:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
name: Retrieve the GitHub Action artifact URLs to install in InstaWP
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: 8.1
coverage: none
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: "ramsey/composer-install@v2"
- name: Retrieve artifact URLs from GitHub workflow
uses: actions/github-script@v6
id: artifact-url
with:
script: |
const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
const artifactURLs = allArtifacts.data.artifacts.map((artifact) => {
return artifact.url.replace('https://api.github.com/repos', 'https://nightly.link') + '.zip'
}).concat([
"https://downloads.wordpress.org/plugin/gatographql.latest-stable.zip"
]);
return artifactURLs.join(',');
result-encoding: string
- name: Artifact URL for InstaWP
run: echo "Artifact URL for InstaWP - ${{ steps.artifact-url.outputs.result }}"
shell: bash
outputs:
artifact_url: ${{ steps.artifact-url.outputs.result }}
process:
needs: provide_data
name: Launch InstaWP site from template 'integration-tests' and execute integration tests against it
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: 8.1
coverage: none
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: "ramsey/composer-install@v2"
- name: Create InstaWP instance
uses: instawp/wordpress-testing-automation@main
id: create-instawp
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INSTAWP_TOKEN: ${{ secrets.INSTAWP_TOKEN }}
INSTAWP_TEMPLATE_SLUG: "integration-tests"
REPO_ID: 25
INSTAWP_ACTION: create-site-template
ARTIFACT_URL: ${{ needs.provide_data.outputs.artifact_url }}
- name: InstaWP instance URL
run: echo "InstaWP instance URL - ${{ steps.create-instawp.outputs.instawp_url }}"
shell: bash
- name: Extract InstaWP domain
id: extract-instawp-domain
run: |
instawp_domain="$(echo "${{ steps.create-instawp.outputs.instawp_url }}" | sed -e s#https://##)"
echo "instawp-domain=$(echo $instawp_domain)" >> $GITHUB_OUTPUT
- name: Run tests
run: |
INTEGRATION_TESTS_WEBSERVER_DOMAIN=${{ steps.extract-instawp-domain.outputs.instawp-domain }}
INTEGRATION_TESTS_AUTHENTICATED_ADMIN_USER_USERNAME=${{ steps.create-instawp.outputs.iwp_wp_username }}
INTEGRATION_TESTS_AUTHENTICATED_ADMIN_USER_PASSWORD=${{ steps.create-instawp.outputs.iwp_wp_password }}
vendor/bin/phpunit --filter=Integration
- name: Destroy InstaWP instance
uses: instawp/wordpress-testing-automation@main
id: destroy-instawp
if: ${{ always() }}
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INSTAWP_TOKEN: ${{ secrets.INSTAWP_TOKEN }}
INSTAWP_TEMPLATE_SLUG: "integration-tests"
REPO_ID: 25
INSTAWP_ACTION: destroy-site
This workflow accesses the .zip file via Nightly Link, a service that allows accessing an artifact from GitHub without logging in, simplifying the configuration of InstaWP.
Releasing the extension plugin
We can provide tools to help release the extensions, automating the procedures as much as possible.
The Monorepo Builder is a library for managing any PHP project, including a WordPress plugin. It provides the monorepo-builder release
command to release a version of the project, incrementing either the major, minor, or patch component of the version according to semantic versioning.
This command executes a series of release workers, which are PHP classes that execute certain logic. The default workers include one that creates a git tag
with the new version and another that pushes the tag to the remote repository. Custom workers can be injected before, after, or in between these steps.
The release workers are configured via a configuration file:
use SymplifyMonorepoBuilderConfigMBConfig;
use SymplifyMonorepoBuilderReleaseReleaseWorkerAddTagToChangelogReleaseWorker;
use SymplifyMonorepoBuilderReleaseReleaseWorkerPushNextDevReleaseWorker;
use SymplifyMonorepoBuilderReleaseReleaseWorkerPushTagReleaseWorker;
use SymplifyMonorepoBuilderReleaseReleaseWorkerSetCurrentMutualDependenciesReleaseWorker;
use SymplifyMonorepoBuilderReleaseReleaseWorkerSetNextMutualDependenciesReleaseWorker;
use SymplifyMonorepoBuilderReleaseReleaseWorkerTagVersionReleaseWorker;
use SymplifyMonorepoBuilderReleaseReleaseWorkerUpdateBranchAliasReleaseWorker;
use SymplifyMonorepoBuilderReleaseReleaseWorkerUpdateReplaceReleaseWorker;
return static function (MBConfig $mbConfig): void {
// release workers - in order to execute
$mbConfig->workers([
UpdateReplaceReleaseWorker::class,
SetCurrentMutualDependenciesReleaseWorker::class,
AddTagToChangelogReleaseWorker::class,
TagVersionReleaseWorker::class,
PushTagReleaseWorker::class,
SetNextMutualDependenciesReleaseWorker::class,
UpdateBranchAliasReleaseWorker::class,
PushNextDevReleaseWorker::class,
]);
};
We can provide custom release workers to augment the release process tailored to the needs of a WordPress plugin. For example, the InjectStableTagVersionInPluginReadmeFileReleaseWorker
sets the new version as the “Stable tag” entry in the extension’s readme.txt file:
use NetteUtilsStrings;
use PharIoVersionVersion;
use SymplifySmartFileSystemSmartFileInfo;
use SymplifySmartFileSystemSmartFileSystem;
class InjectStableTagVersionInPluginReadmeFileReleaseWorker implements ReleaseWorkerInterface
{
public function __construct(
// This class is provided by the Monorepo Builder
private SmartFileSystem $smartFileSystem,
) {
}
public function getDescription(Version $version): string
{
return 'Have the "Stable tag" point to the new version in the plugin's readme.txt file';
}
public function work(Version $version): void
{
$replacements = [
'/Stable tag:s+[a-z0-9.-]+/' => 'Stable tag: ' . $version->getVersionString(),
];
$this->replaceContentInFiles(['/readme.txt'], $replacements);
}
/**
* @param string[] $files
* @param array $regexPatternReplacements regex pattern to search, and its replacement
*/
protected function replaceContentInFiles(array $files, array $regexPatternReplacements): void
{
foreach ($files as $file) {
$fileContent = $this->smartFileSystem->readFile($file);
foreach ($regexPatternReplacements as $regexPattern => $replacement) {
$fileContent = Strings::replace($fileContent, $regexPattern, $replacement);
}
$this->smartFileSystem->dumpFile($file, $fileContent);
}
}
}
By adding InjectStableTagVersionInPluginReadmeFileReleaseWorker
to the configuration list, whenever executing the monorepo-builder release
command to release a new version of the plugin, the “Stable tag” in the extension’s readme.txt file will be automatically updated.
Publishing the extension plugin to the WP.org directory
We can also distribute a workflow to help release the extension to the WordPress Plugin Directory. When tagging the project on the remote repository, the following workflow will publish the WordPress extension plugin to the directory:
# See: https://github.com/10up/action-wordpress-plugin-deploy#deploy-on-pushing-a-new-tag
name: Deploy to WordPress.org Plugin Directory (SVN)
on:
push:
tags:
- "*"
jobs:
tag:
name: New tag
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: WordPress Plugin Deploy
uses: 10up/action-wordpress-plugin-deploy@stable
env:
SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
SLUG: ${{ secrets.SLUG }}
This workflow uses the 10up/action-wordpress-plugin-deploy
action, which retrieves the code from a Git repository and pushes it to the WordPress.org SVN repository, simplifying the operation.
Summary
When creating an extensible plugin for WordPress, our goal is to make it as easy as possible for third-party developers to extend it, thereby maximizing the chances of fostering a vibrant ecosystem around our plugins.
While providing extensive documentation can guide developers on how to extend the plugin, an even more effective approach is to supply the necessary PHP code and tooling for development, testing, and releasing their extensions.
By including the additional code needed by extensions directly in our plugin, we simplify the process for developers.
Do you plan to make your WordPress plugin extensible? Let us know in the comments section.
WORDPRESS
6 Surprising Things You Can Do on WordPress.com Without a Plugin – WordPress.com News
“WordPress” and “plugins” are often thought to be synonymous. The two go together like peanut butter and jam, like summer and watermelon, like yin and yang . . . you get the idea. It often seems like you can’t have one without the other. While plugins are indeed one way to supercharge your WordPress.com site, the truth is that there’s a lot you can do without ever installing a plugin. In fact, I can almost guarantee that our out-of-the-box WordPress.com experience is more powerful than you think.
(Quick reminder: WordPress plugins are available and installable on our Creator and Entrepreneur plans.)
Today, we’d like to highlight six surprising things you can do with WordPress.com from the moment you start building a website.
Sell digital products and accept donations
There’s no plugin needed to make a living or earn some cash on the side by selling digital products like ebooks, songs, or photographs on WordPress.com. With our built-in payment blocks Payment Buttons, Pay with PayPal, and Donations Form, you’re one click away from collecting money on your website. Best of all? Most of these blocks can be used on any plan, including Free, the exception being the PayPal block, which requires the Explorer plan or above.
Simply connect your Stripe account to get started selling today.
Prevent spam
Just like the spam comments and messages you’re trying to block, the number of anti-spam plugins has proliferated in recent years. Luckily, you don’t need any of them, because Akismet, an Automattic product, is baked into every WordPress.com website and provided at no extra cost. With advanced filtering that blocks 99.99% of spam, you’ll never have to worry about unwanted visitors again.
Install SSL certificates
On WordPress.com websites, SSL certificates are provided free of charge and automatically installed for you. This feature provides important security against hackers and other malicious actors, particularly if your website collects user information of any kind. At other hosts, you’ll often have to either pay extra or install your own (expensive) plugin in order to add an SSL certificate. Not at WordPress.com. Learn more about our SSL process here.
Send newsletters
Since 2009 WordPress.com has had the built-in functionality of sending new posts as emails. That’s right, you don’t need a third-party service or platform (like Mailchimp or Substack) to send newsletter emails to your audience. Using a Subscribe block gives visitors a simple and convenient way to enter their email and get your posts right to their inbox.
You can also set up paywalls (with the Paywall block) and paid content tiers, allowing for multiple subscription levels. Additionally, you can view and manage subscriber details from the Subscribers page (found under “Jetpack” on the left-side menu). Learn more at WordPress.com/newsletters.
Embed videos
Videos can be a vitally important part of your website and content flow, but uploading them can be a pain in the neck—if you’re not using WordPress, that is. If you’re embedding a video from another source, like Vimeo or YouTube, use our built-in blocks of the same name. And here’s a helpful tip: you don’t even need to select the block first. Simply copy and paste the video link right into the editor, and WordPress will automagically do the rest.
For embedding your original video files (.mov, .mp4, .mwv, etc.), Automattic’s very own VideoPress block offers a straightforward and robust solution. With caption and chapter support, as well as detailed data and insights on views, once you try out VideoPress you won’t look back. This feature is available on Explorer plans and above.
Insert impressive image galleries
Well-done imagery on a website can mean the difference between an engaged visitor and a bounced visitor. Rather than experimenting with overly complicated plugins, use the various image blocks that come with WordPress. Our Gallery, Slideshow, and Image Compare blocks are especially fun and offer a range of easy-to-use customizations that don’t overwhelm. Plus, these blocks are always optimized for mobile.
Start building today
This is just a sampling of what you can do with WordPress.com. We didn’t even mention some of our favorite blocks, including: table of contents, music/podcast player, countdown timer, tables, and so much more.
Ready to explore these powerful built-ins? Get started today:
You may still find that using plugins solves your specific needs a bit better than what’s already built into the editor. If that’s the case, consider our world-class Creator or Entrepreneur plan.
Join 111.6M other subscribers
WORDPRESS
New WordPress.com Themes for June 2024 – WordPress.com News
Five of our favorite new themes.
The WordPress.com team is always working on new design ideas to bring your website to life. Check out the latest themes in our library, including great options for crafters, legal firms, and artists.
Craftfully is a magazine-style WordPress theme designed for all things homemade, DIY, crafts, and hobbies. Utilizing plenty of white pace, lighter colors, and playful fonts, this theme delightfully evokes a sense of playfulness and DIY creativity. Below a prominent featured section and newsletter sign-up box, a two-column layout displays the “Latest Posts” as well as a short bio and social links. Craftfully is more than just a theme, it’s a canvas for your imagination.
Click here to view a demo of this theme.
Though Portia is simple in its design and layout, it sends a powerful message: your firm is the best in the industry. With a focus on conveying professionalism, stability, and dependability, Portia is built with legal firms in mind, but is versatile enough to work for any business that relies on trustworthiness and respectability. You won’t find any rock-the-boat design elements here. It’s all about communicating the right information at the right time.
Click here to view a demo of this theme.
Kiosko is a sleek and modern WooCommerce theme tailored for online stores specializing in art prints and home goods. With its high contrast black and white design, Kiosko offers a minimalist aesthetic that puts the spotlight on your products. It’s straightforward and effective, meaning you can focus on your art and your sales rather than your infrastructure.
Click here to view a demo of this theme.
Dark Academia is a blog theme with a dark, moody aesthetic. Its sophisticated layout will especially stand out to visitors. The sticky left half features your site’s name and the primary navigation menu, while the right half scrolls through your latest posts. This style is perfect for blogs focused on literature, history, and fashion, and it’s naturally deal for those who appreciate the Dark Academia vibe. The elegant and immersive reading experience will make your site memorable for anyone who happens by it.
Click here to view a demo of this theme.
OnyxPulse is a sleek WordPress theme with a modern, minimalist design, perfect for blogs about design, future trends, and innovation. Its grid layout and high-contrast visuals are ideal for showcasing cutting-edge content and engaging a tech-savvy audience. The striking black-and-white color palette and sharp design elements are sure to catch your visitors’ eye. For the typography, we’ve opted for Chakra Petch, a square sans-serif font; its sharply tapered corners are a perfect match for this theme.
Click here to view a demo of this theme.
To install any of the above themes, click the name of the theme you like, which brings you right to the installation page. Then click the “Activate this design” button. You can also click “Open live demo,” which brings up a clickable, scrollable version of the theme for you to preview.
Premium themes are available to use at no extra charge for customers on the Explorer plan or above. Partner themes are third-party products that can be purchased for $99/year each on the Creator plan and above.
You can explore all of our themes by navigating to the “Themes” page, which is found under “Appearance” in the left-side menu of your WordPress.com dashboard. Or you can click below:
Join 111.5M other subscribers
WORDPRESS
Five Takeaways from WordCamp Europe 2024 (From a First-Time WordCamp Attendee) – WordPress.com News
This year’s WordCamp Europe was held in Torino, Italy, the capital city of the Piedmont region in northern Italy. Torino is known for its rich automotive history, beautiful architecture, and, of course, incredible food.
From June 13-15, 2024, over 2,500 folks from the WordPress community, including many of us from the WordPress.com team, came together to learn, connect, and give back to the WordPress project that powers over 43% of the entire internet.
I joined the WordPress.com team back in January of this year, so WordCamp Europe 2024 was my first WordCamp experience. In today’s post, I thought it might be interesting to hear about the conference from a first-timer, especially if you’re considering attending a WordCamp or WordPress meetup in the future.
Here are my top five takeaways from my very first WordCamp:
1. In-person connection is powerful.
If your typical workday looks similar to mine––sitting at a desk at your house all day by yourself––going to a conference as large as WordCamp Europe may be a wee bit out of your comfort zone. It certainly was out of mine.
That said, I’ve recently found myself craving in-person connection after the pandemic and working almost exclusively from home for over eight years. Not only did attending this conference just get me out of my normal routine, it allowed me to connect with folks who love the tool I’ve used personally and professionally for over a decade: WordPress.
I staffed the WordPress.com booth, so I had a ton of opportunities to chat with other business owners, developers, creators, and makers over the course of the conference. I actually talked with a few fellow self-taught women developers like me, and I walked away feeling inspired, motivated, and just really thankful to be a part of this community.
But the best conversations happened in places I wouldn’t have expected: over spritzes, grabbing a cafe at the venue, or just walking around the city.
Embracing connection was a big focus of Matt’s final keynote speech during the event, and I couldn’t agree more. Events like WordCamps allow for swapping ideas, collaborating and troubleshooting, and experiencing a sense of community that you just don’t get while sitting behind a computer.
That said, if large-scale conferences like a flagship WordCamp just aren’t for you, try checking out a local WordPress meetup to connect with other like-minded folks in your community.
2. It takes a ton of people to make WordCamps great.
One of the most moving parts of WordCamp Europe was at the very end when all of the volunteers and organizers were called to the stage; it’s truly amazing just how many people need to be involved to make an event like WordCamp actually happen.
Everything was smooth and well-organized, and the volunteers and organizers could not have been more helpful. Their enthusiasm about the event, WordPress, and community in general throughout the conference was infectious.
WordCamps and local WordPress meetups are always looking for volunteers; donating your time and expertise for events and meetups like this are a great way to give back to the WordPress project and community.
And if you’re an organizer of your local WordPress meetup, check out this post for information on how you can get a free WordPress.com website for your local meetup.
3. Contributor Day isn’t intimidating for a first-timer.
I’ve never contributed to WordPress core, but it was one of my goals for this year. That’s why I was so excited to participate in Contributor Day at WordCamp Europe.
If you’re unfamiliar with Contributor Day, it’s an event that usually kicks off a WordCamp. Teams focus on contributing to the WordPress open source project, with groups focused on code, support, translations, sustainability, inclusion, and more.
After listening to all of the team presentations, I decided to join the Accessibility team. Accessibility is something that has always interested me, but it’s also something I don’t have a ton of experience with.
That said, my inexperience wasn’t just accepted, it was actively welcomed.
Once I got to the Accessibility team table, I was immediately greeted and welcomed. Then I paired with a fellow contributor, Marco Acato from Acato Digital Agency, to test the accessibility of a new theme for the WordPress theme repository.
I learned so much, asked a ton of questions, and felt surprisingly accomplished after just a few hours of testing this theme. We were actually able to publish feedback for the theme developer at the end of the day as well.
Contributor Day gave me an even deeper appreciation for the entire community that supports the WordPress project every single day. So much work and effort goes into maintaining and improving this tool that millions of websites across the world rely on to run their businesses, amplify their messages, and stay in touch with others. I felt so grateful to have been a part of it during Contributor Day and would encourage any other first-timers to attend a Contributor Day in the future as well.
Luckily, WordPress core is always looking for volunteers and contributors; check out this guide or the new Contributor Mentorship Program if you’re interested in becoming a contributor yourself.
4. Torino was a great host city, and the WordCamp team made navigating a new city easy.
Between attending Contributor Day and sessions, to checking out sponsor booths and attending side events, I didn’t think we’d have a ton of time to actually see the city or Torino.
I actually had plenty of time to explore with my coworkers, eat pizza every single day, and scope out the best gelato spots outside of conference hours.
The WordCamp Europe team did a great job preparing attendees to make the most of our time in the city as well; their travel guides helped me feel confident navigating the city and finding some of the foods that come from this area in Italy.
As a first-timer in Torino, I really appreciated the extra work that the WordCamp team did to ensure everyone had a chance to explore and experience the city.
5. Pizza really is poetry.
I would be remiss to not mention the food that we ate during our time in our host city! While we like to say that “Code is poetry” around here, so is pizza.
One of my very favorite memories from the event was the branding. The design team for WordCamp Europe 2024 added subtle nods to our host country throughout the venue, which was incredibly clever and well-done.
And while pizza is indeed poetry, it’s even better when shared amongst coworkers, friends, and people who get excited about the same things that excite you.
Wrapping up
I loved my time at WordCamp Europe, and I’m already looking forward to the next time I can connect face-to-face with the WordPress community.
Were you at WordCamp Europe this year? Leave a comment with your favorite memory from the event below.
Join 111.4M other subscribers
-
SEO5 days ago
Google Clarifies Organization Merchant Returns Structured Data
-
SEO7 days ago
Google Dials Back AI Overviews In Search Results, Study Finds
-
AFFILIATE MARKETING5 days ago
The 7-Step ChatGPT Formula for Peak Productivity and Profit
-
SEARCHENGINES5 days ago
Daily Search Forum Recap: June 21, 2024
-
AFFILIATE MARKETING6 days ago
How I Hit $100 Million in Annual Revenue By Being More Transparent
-
SEARCHENGINES4 days ago
Google Spam Update, Volatility & Indexing Bug, AI Overviews Tracking & FAQs, Google Maps Exploit, Google Ads, AppleBot & More
-
SEARCHENGINES6 days ago
Google Search Not Indexing & Serving New Content?
-
WORDPRESS7 days ago
Five Takeaways from WordCamp Europe 2024 (From a First-Time WordCamp Attendee) – WordPress.com News