This guide provides detailed instructions on how to create a plugin, including defining a custom Blog entity, creating an administrative grid, and integrating with the Shopware admin panel.
Prerequisites
Before starting, ensure that you have the following:
- Shopware 6.6 installed on your local or server environment.
- Basic knowledge of PHP, Shopware plugin structure, and Symfony components.
- Command-line tools and access to the Shopware installation directory.
- How create plugin in Shopware 6.6.
Plugin Structure
Blog/
├── composer.json
└── src/
├── Blog.php
├── Core/
│ └── Content/
│ └── Blog/
│ ├── BlogEntity.php
│ ├── BlogDefinition.php
│ └── BlogCollection.php
├── Migration/
│ └── Migration1733387634CreateBlogTable.php
└── Resources/
├── config/
│ └── services.xml
└── app/
└── administration/
└── src/
├── main.js
└── module/
└── admin-grid/
├── index.js
└── page/
├── admin-grid-list/
│ ├── index.js
│ └── admin-grid-list.html.twig
└── admin-grid-add/
├── index.js
└── admin-grid-add.html.twig
Step 1: Create the Plugin
1.1 Create plugin
To create a plugin, you can use the command:
bin/console plugin:create Blog
Blogis the name of our module.- You must enter the namespace after executing the aforementioned command. The term “
Vendor” was used in our instance. - Additionally, “
no” must be the response to every subsequent question. - Following plugin creation, the generated files should appear in a folder named Blog.
1.2 Activate plugin
It is necessary to install and activate the plugin after it has been created, but we will do this last, after all the files have been added.
You must update the plugin list with the first command before using the second command to install and activate a plugin:
bin/console plugin:refresh
bin/console plugin:install --activate EntityExample
You will have an empty plugin with the namespace Vendor and the name Blog after completing the aforementioned procedures.
Step 2: Define the Entity
A table must be created in the database before an entity can be created; to do this, a migration is made.
2.1 Create Database Migration
2.1.1 Create migraion file
With the following command, you can generate a migration file in your plugin for additional editing:
bin/console database:create-migration -p Blog --name CreateBlogTable
The file src/Migration/Migration/Migration1733387634CreateBlogTable.php will be created in your plugin after executing the aforementioned command:
<?php declare(strict_types=1);
namespace Vendor\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
/**
* @internal
*/
class Migration1733387634CreateBlogTable extends MigrationStep
{
public function getCreationTimestamp(): int
{
return 1733387634;
}
public function update(Connection $connection): void
{
}
}
2.1.2 Update migraion file
A new table is created in the database by updating the “update” method in the file after it is created:
public function update(Connection $connection): void
{
$sql = <<<SQL
CREATE TABLE IF NOT EXISTS `blog` (
`id` BINARY(16) NOT NULL,
`title` VARCHAR(255) COLLATE utf8mb4_unicode_ci,
`description` TEXT COLLATE utf8mb4_unicode_ci,
`image_id` BINARY(16) NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3),
PRIMARY KEY (`id`),
CONSTRAINT `blog_media_id_fk` FOREIGN KEY (`image_id`)
REFERENCES `media` (`id`)
ON DELETE SET NULL
ON UPDATE CASCADE
)
ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
SQL;
$connection->executeStatement($sql);
}
The plugin will now generate a blog table in the database after it has been installed and activated. The following command can also be used:
bin/console database:migrate Blog --all
Note: For more details on how to create migration files and why they are needed, please refer to the official documentation of the developer.
2.2 Entity Classes
Once a table has been created in the database, an entity can be created.
To create an entity, we need to create three files in folder src/Core/Content/Blog/:
BlogEntity.php: a lightweight data object that serves as a container for properties corresponding to the fields defined in the entity, excluding the ID field, which is managed using theEntityIdTrait.BlogDefinition.php: defines the structure of the entity, including its fields and name, which also serves as the database table name. The name must match exactly with the corresponding table in the database.BlogCollection.php: a collection ofBlogEntityobjects, acting as a container to manage multiple instances of the entity. It provides utility methods for iterating, filtering, and accessing the containedBlogEntityobjects in bulk. This class is typically used to handle lists of entities returned from queries or for batch operations.
BlogEntity.php:
<?php declare(strict_types=1);
namespace Vendor\Core\Content\Blog;
use Shopware\Core\Framework\DataAbstractionLayer\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityIdTrait;
class BlogEntity extends Entity
{
use EntityIdTrait;
protected ?string $title;
protected ?string $description;
protected ?string $image_id;
/**
* @return string|null
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* @param string|null $title
*/
public function setTitle(?string $title): void
{
$this->title = $title;
}
/**
* @return string|null
*/
public function getDescription(): ?string
{
return $this->description;
}
/**
* @param string|null $description
*/
public function setDescription(?string $description): void
{
$this->description = $description;
}
/**
* @return string|null
*/
public function getImageId(): ?string
{
return $this->image_id;
}
/**
* @param string|null $image_id
*/
public function setImageId(?string $image_id): void
{
$this->image_id = $image_id;
}
}
BlogDefinition.php:
<?php declare(strict_types=1);
namespace Vendor\Core\Content\Blog;
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\LongTextField;
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\StringField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
use Shopware\Core\Content\Media\MediaDefinition;
class BlogDefinition extends EntityDefinition
{
public const ENTITY_NAME = 'blog';
public function getEntityName(): string
{
return self::ENTITY_NAME;
}
public function getEntityClass(): string
{
return BlogEntity::class;
}
public function getCollectionClass(): string
{
return BlogCollection::class;
}
protected function defineFields(): FieldCollection
{
return new FieldCollection([
(new IdField('id', 'id'))->addFlags(new Required(), new PrimaryKey()),
new StringField('title', 'title'),
new LongTextField('description', 'description'),
(new FkField('image_id', 'imageId', MediaDefinition::class)),
new OneToOneAssociationField(
'image',
'image_id',
'id',
MediaDefinition::class,
false
),
]);
}
}
BlogCollection.php:
<?php declare(strict_types=1);
namespace Vendor\Core\Content\Blog;
use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
/**
* @method void add(BlogEntity $entity)
* @method void set(string $key, BlogEntity $entity)
* @method BlogEntity[] getIterator()
* @method BlogEntity[] getElements()
* @method BlogEntity|null get(string $key)
* @method BlogEntity|null first()
* @method BlogEntity|null last()
*/
class BlogCollection extends EntityCollection
{
protected function getExpectedClass(): string
{
return BlogEntity::class;
}
}
Note: You can find out how to create entities in the developer’s documentation.
2.3 Register Services
After we have created an entity, it should be registered in the services.
Update src/Resources/config/services.xml to register the entity with Shopware’s DI container.
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Vendor\Core\Content\Blog\BlogDefinition">
<tag name="shopware.entity.definition" entity="blog" />
</service>
</services>
</container>
Each service is connected with a service tag. In our case we are registering a new entity; for this we need:
- in the id parameter, set the path to our definition file.
- also, we need to add a tag with the name
shopware.entity.definitionand the entity blog in our case. In other cases, the entity may have a different meaning.
Note: You can see how to connect dependencies here.
2.4 Deleting entity data with plugin removal
Also, if the user wants to remove the data when deleting the plugin, you need to add additional logic to the Blog.php plugin file:
<?php declare(strict_types=1);
namespace Vendor;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use RuntimeException;
use Shopware\Core\Framework\Plugin;
use Shopware\Core\Framework\Plugin\Context\UninstallContext;
use Vendor\Core\Content\Blog\BlogDefinition;
class Blog extends Plugin
{
public function uninstall(UninstallContext $uninstallContext): void
{
parent::uninstall($uninstallContext);
if ($uninstallContext->keepUserData()) {
return;
}
$this->deleteBlogTable();
}
/**
* Deletes the blog table
*
* @return void
*/
protected function deleteBlogTable(): void
{
$tableName = BlogDefinition::ENTITY_NAME;
/** @var Connection $connection */
$connection = $this->container->get(Connection::class);
try {
$connection->executeStatement("DROP TABLE IF EXISTS `$tableName`;");
} catch (Exception $e) {
throw new RuntimeException('Could not delete table ' . $tableName, 0, $e);
}
}
}
In the code above we create the deleteBlogTable method to delete the table from the database. And in the uninstall method, if the user wants to delete the data, the deleteBlogTable method will be called and delete the table.
Step 3: Administration
3.1 Add Administration Module
To create pages in the admin panel we need to create a module. You can find out how to create a module and for what purpose in the documentation.
To create a module we need to create a folder src/Resources/app/administration/src in the plugin.
Next we need to connect our module in the file src/Resources/app/administration/src/main.js:
import './module/admin-grid';
Then register the module and configure itsrc/Resources/app/administration/src/module/admin-grid/index.js:
import './page/admin-grid-list';
import './page/admin-grid-add';
Shopware.Module.register('admin-grid', {
type: 'plugin',
name: 'admin-grid',
title: 'admin-grid.general.mainMenuItemGeneral',
description: 'admin-grid.general.descriptionTextModule',
color: '#9AA8B5',
routes: {
list: {
component: 'admin-grid-list',
path: 'list'
},
add: {
component: 'admin-grid-add',
path: 'add/:id?',
props: {
default: (route) => ({ id: route.params.id }),
},
meta: {
parentPath: 'admin.grid.list',
},
},
},
navigation: [{
id: 'admin-grid-list',
label: 'Admin Grid',
color: '#9AA8B5',
path: 'admin.grid.list',
icon: 'default-action-settings',
parent: 'sw-content',
position: 100
}]
});
In the file above, you need to specify the parameters:
- type – the
typewould be ‘plugin’ here; - name – the
nameshould be a technical unique one; - title – (optional) the name of the module to be displayed;
- description – (optional) module description;
- routes – object containing multiple route configuration objects, is needed to define pages in this module;
- navigation – array of objects, each one configuring a route connected to your module, is needed to add an item to the menu in the sidebar of the admin panel.
At this stage if we use the bin/build-administration.sh command we can see the new menu item, but it will not lead anywhere because we haven’t created any pages yet:

You can learn more about the module here, also about custom routes and navigation.
3.2 Blog page component with grid
3.2.1 Admin-grid-list component
Now we can create a component/page to display the grid. To do this we need to create a folder module/admin-grid/page/admin-grid-list.
Then in it we need to create index.js file in which we will register the component:
import template from './admin-grid-list.html.twig';
import './admin-grid-list.scss';
const { Component, Mixin } = Shopware;
const { Criteria } = Shopware.Data;
const ERROR_FETCHING_DATA = 'Error fetching data';
const ERROR_DELETING_ITEM = 'Error deleting item';
const SUCCESS_DELETING_ITEM = 'Item deleted successfully';
Component.register('admin-grid-list', {
template,
inject: [
'repositoryFactory',
'filterFactory',
],
mixins: [
Mixin.getByName('notification'),
Mixin.getByName('listing'),
],
data() {
return {
entity: null,
columns: [
{ property: 'title', label: 'Title' },
{ property: 'description', label: 'Description' },
{ property: 'imageId', label: 'Image' },
],
isLoading: true,
limit: 5,
filterCriteria: [],
defaultFilters: [
'title',
'description',
'hasImage',
],
storeKey: 'grid.filter.blog',
activeFilterNumber: 0,
};
},
metaInfo() {
return {
title: this.$createTitle(),
};
},
computed: {
entityRepository() {
return this.repositoryFactory.create('blog');
},
listFilterOptions() {
return {
'title': {
property: 'title',
label: 'Title',
placeholder: 'Title',
},
'description': {
property: 'description',
label: 'Description',
placeholder: 'Description',
},
'hasImage': {
property: 'imageId',
label: 'Has Image',
type: 'existence-filter',
optionHasCriteria: 'Has',
optionNoCriteria: 'No',
},
};
},
listFilters() {
return this.filterFactory.create('blog', this.listFilterOptions);
},
entityCriteria() {
const entityCriteria = new Criteria(this.page, this.limit);
if (this.term) {
entityCriteria.addFilter(
Criteria.multi('OR', [
Criteria.contains('title', this.term),
Criteria.contains('description', this.term)
])
);
}
this.filterCriteria.forEach(filter => {
entityCriteria.addFilter(filter);
});
return entityCriteria;
},
},
watch: {
entityCriteria: {
handler() {
this.getList();
},
deep: true,
},
},
methods: {
async getList() {
this.isLoading = true;
try {
const repository = this.entityRepository;
let criteria = await Shopware.Service('filterService')
.mergeWithStoredFilters(this.storeKey, this.entityCriteria);
this.activeFilterNumber = criteria.filters.length;
const result = await repository.search(criteria, Shopware.Context.api);
this.entity = result;
this.total = result.total;
} catch (error) {
this.handleError(ERROR_FETCHING_DATA, error);
} finally {
this.isLoading = false;
}
},
async deleteItem(itemId) {
this.isLoading = true;
try {
const repository = this.entityRepository;
await repository.delete(itemId, Shopware.Context.api);
await this.getList();
this.createNotificationSuccess({
title: 'Success',
message: SUCCESS_DELETING_ITEM,
});
} catch (error) {
this.handleError(ERROR_DELETING_ITEM, error);
} finally {
this.isLoading = false;
}
},
editItem(itemId = null) {
const routeName = 'admin.grid.add';
this.$router.push({ name: routeName, params: { id: itemId } });
},
handleError(message, error) {
this.createNotificationError({
title: 'Error',
message,
});
},
updateCriteria(criteria) {
this.page = 1;
this.filterCriteria = criteria;
},
}
});
There are many parts in the file above:
- template – is a connected external file with a page template;
- data – is the data we’re initialising;
- computed – similar to data, except it can also perform custom logic to initialise a variable;
- metaInfo – this method is needed to change the name of the page.
- watch – observer that allows you to track changes in the remonstrate and apply custom logic;
- methods – this is where methods are created;
- inject – here you can connect external services;
- mixins – here you can connect the mixins.
Read more about template, data, computed and watch, inject, mixins.
Grid
To display entity data in the grid, we need to connect a repository, this can be done with the repositoryFactory injection.
You need to create a columns variable that will contain the settings for each column in the grid. Next, we need to create a entityRepository variable that will get our entity’s data from the repository.
Next, we need to create a getList method that will process our data and save it to a variable. In this method also partially implements search, filter and pagination functionality.
Pagination and search
Pagination, search and filters require mixin “listing”. You can see what functionality this mixin provides here.
Filters
For filters we need variables: filterCriteria, defaultFilters, storeKey, activeFilterNumber.
The next step is to establish filter choices, which requires two techniques: listFilterOptions, listFilters in computed.
The entityCriteria method in computed is needed for both search and filters, it creates a variable entityCriteria which stores Criteria with already set filters and their values.
Then we need a watch for entityCriteria so that when we change filters or search we get the corresponding data.
And finally we need the updateCriteria method to retrieve the filters from the component in the template.
3.2.2 Admin-grid-list template
Also for the page we need template admin-grid-list.html.twig:
{% block sw_data_grid %}
<sw-page class="sw-container">
{% block sw_entity_list_search_bar %}
<template #search-bar>
<sw-search-bar
initial-search-type="blog"
:initial-search="term"
@search="onSearch"
/>
</template>
{% endblock %}
{% block sw_entity_list_content %}
<template #content>
<sw-card title="Blog Grid" id="grid-listening">
<sw-card-section>
<sw-data-grid
:dataSource="entity"
:columns="columns"
:showSelection="false"
:isLoading="isLoading"
identifier="title"
>
<template #column-imageId="props">
<div v-if="props.item.imageId" class="custom-image-column">
<sw-media-preview-v2
:source="props.item.imageId"
/>
</div>
<div v-else>
No Image
</div>
</template>
<template #actions="{ item }">
<sw-button v-on:click="editItem(item.id)">Edit</sw-button>
<sw-button v-on:click="deleteItem(item.id)" class="sw-context-menu-item--danger">Delete</sw-button>
</template>
<template #pagination>
<sw-pagination
v-bind="{ limit, page, total }"
:auto-hide="false"
:steps="[5, 10, 25, 50]"
@page-change="onPageChange"
/>
</template>
</sw-data-grid>
</sw-card-section>
<sw-card-section>
<sw-button @click="editItem()" variant="primary">Add New Post</sw-button>
</sw-card-section>
</sw-card>
</template>
{% endblock %}
{% block sw_entity_list_sidebar %}
<template #sidebar>
<sw-sidebar class="sw-order-list__sidebar">
<sw-sidebar-filter-panel
entity="entity"
:store-key="storeKey"
:active-filter-number="activeFilterNumber"
:filters="listFilters"
:defaults="defaultFilters"
@criteria-changed="updateCriteria"
/>
</sw-sidebar>
</template>
{% endblock %}
</sw-page>
{% endblock %}
We use base components in this file; you can read more about them here. You should also pay attention to the template tags, where it is located, how and with what values, it is important.
Grid
To display the grid, we need a basic sw-data-grid component. In it we pass:
dataSource– the data of our entity;columns– the settings for columns;isLoading– the variable which is responsible for displaying the unloading status;- set the value for
showSelection–false; - set the
identifieron the name field.
Pagination
For pagination we use the basic sw-pagination component in which we pass:
limit,page,total– the variables;- set the values for
auto-hidetofalse; - set the values for
stepswith the array; onPageChange– method which is located in the mixin “listing”.
Search
For search we need a basic component sw-search-bar, in it we need to pass:
- the entity to be searched;
term– a variable to record the search value;onSearch– function for updateterm.
The variable term and onSearch are in the mixin “listing”.
Filters
Filters, for them you need a basic sw-sidebar-filter-panel component in the sw-sidebar component . You need to pass to it:
entity– the data of our entity;storeKey– filter key;activeFilterNumber– number of active filters, for CSS;listFilters– filter option settings;defaultFilters– filter option names;updateCriteria– method for updating filter data.
Display image
In order to display a image you need to create a temple in which to pass in the props with the image id and pass in the base component sw-media-preview-v2 this identifier.
3.2.3 Result
After creating this page, we need to re-bulding the administration, and we can now navigate to it in the menu as described above and get this result grid with example:

3.3 Add/Edit Page Components
We now require a page in the admin-grid-add folder where we can add and edit new posts.
3.3.1 Admin-grid-add component
Create admin-grid-add/index.js component:
import template from './admin-grid-add.html.twig';
const { Component, Mixin } = Shopware;
Component.register('admin-grid-add', {
template,
inject: ['repositoryFactory'],
mixins: [
Mixin.getByName('notification')
],
props: {
id: {
type: String,
required: false,
default: null,
},
},
data() {
return {
entity: {},
isLoading: false,
entityLoaded: false,
defaultFolder: '0192c416bc2972ca9b7c19dd609a6bc9',
mediaItem: null,
};
},
created() {
if (this.id && !this.entityLoaded) {
this.loadEntity();
this.entityLoaded = true;
}
},
computed: {
entityRepository() {
return this.repositoryFactory.create('blog');
},
},
methods: {
async loadEntity() {
this.isLoading = true;
try {
const repository = this.entityRepository;
this.entity = await repository.get(this.id, Shopware.Context.api);
if (this.entity.imageId) {
const mediaRepository = this.repositoryFactory.create('media');
this.mediaItem = await mediaRepository.get(this.entity.imageId, Shopware.Context.api);
}
} catch (error) {
console.error('Error loading entity:', error);
} finally {
this.isLoading = false;
}
},
syncFormToEntity(field, value) {
this.entity[field] = value;
},
async saveEntity() {
this.isLoading = true;
try {
const repository = this.entityRepository;
let entity = this.id
? await repository.get(this.id, Shopware.Context.api)
: repository.create(Shopware.Context.api);
entity.title = this.entity.title;
entity.description = this.entity.description;
entity.imageId = this.entity.imageId;
await repository.save(entity, Shopware.Context.api);
this.$router.push({ name: 'admin.grid.list' });
this.createNotificationSuccess({
title: 'Success',
message: 'Entity saved successfully',
});
} catch (error) {
console.error('Error saving entity:', error);
this.createNotificationError({
title: 'Error',
message: 'Failed to save entity',
});
} finally {
this.isLoading = false;
}
},
async onUploadFinish({ targetId }) {
this.entity['imageId'] = targetId;
const mediaRepository = this.repositoryFactory.create('media');
this.mediaItem = await mediaRepository.get(targetId, Shopware.Context.api);
},
onMediaRemove() {
this.mediaItem = null;
},
cancel() {
this.$router.push({ name: 'admin.grid.list' });
},
},
});
Similar to the first page above, we need a repository and our entity from there, for which the variables entityRepository, entity and the loadEntity method will be responsible.
The saveEntity method is needed to save an edited post or a new post.
The syncFormToEntity method saves the value from the form fields to an entity variable before saving it later.
The onUploadFinish and onMediaRemove methods are needed to work with the image.
onUploadFinish– saves the image id for further saving it into the entity and also saves the image itself to display a preview.onMediaRemove– the method simply cancels the selected image to select a new one.
The cancel method moves us to the grid page.
3.3.2 Admin-grid-add template
Next, we need to create the admin-grid-add.html.twig template for adding/editing entities:
<div class="sw-container">
<sw-card v-if="isLoading" :title="'Loading...'">
<sw-loader />
</sw-card>
<sw-card v-else :title="id ? 'Edit Blog Post' : 'Add New Blog Post'">
<sw-text-field
label="Title"
v-model="entity.title"
@blur="syncFormToEntity('title', $event.target.value)"
/>
<sw-textarea-field
label="Description"
v-model="entity.description"
@change="syncFormToEntity('description', $event)"
/>
<sw-media-upload-v2
uploadTag="image-upload"
:allowMultiSelect="false"
:defaultFolder="defaultFolder"
:source="mediaItem"
@media-upload-remove-image="onMediaRemove"
variant="regular"
label="My image-upload">
</sw-media-upload-v2>
<sw-upload-listener
auto-upload
@media-upload-finish="onUploadFinish"
uploadTag="image-upload">
</sw-upload-listener>
<sw-button-group>
<sw-button @click="saveEntity" variant="primary">{{ id ? 'Save Changes' : 'Add Post' }}</sw-button>
<sw-button @click="cancel" variant="ghost">Cancel</sw-button>
</sw-button-group>
</sw-card>
</div>
In this template we use 4 main basic components:
sw-text-field– is a field to save the title of our post;sw-textarea-field– is a field to save the post description;sw-media-upload-v2– this component is needed to save a picture to the media repository.sw-upload-listener– this component tracks changes insw-media-upload-v2and we need it to save the image identifier.
3.3.3 Result
Now after creating a new page you need to re-regenerate the administration. After generation we can go to add a new post by clicking on the “Add Post” button and we will see this page:

Build Administration
After creating all the files you need to install and activate the plugin with the commands that were mentioned above. And also generate new pages in the admin panel with the command:
bin/build-administration.sh
Result and demo
As described above we can go to our grid by the path “Content” -> “Admin Grid”.
Add/Delete post
Let’s start with a demonstration of adding/deleting a new post:
Edit post
Now after adding a new element we can edit it:
Searching and filtering
At the top of the page, highlighted in red, you can see the search that can be used for this grid; it works for the text in the title and description columns.
On the right side, also highlighted in red, you can see filters customisable for this grid; by default, they are closed. To open them, you need to click on the filters button highlighted in red in front of the filters.

Conclusion
As a result, we get a plugin for Shopware 6.6, in which we can view, add, edit and delete posts, as well as we can search and filter by posts.