Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Article Bundle Laravel Package

sulu/article-bundle

SuluArticleBundle adds performance-optimized article management to the Sulu CMS. Articles are localized, content-rich entities that can be edited in the admin interface, organized into separate type-based lists, and rendered on Sulu-powered websites.

View on GitHub
Deep Wiki
Context7

Getting Started

SuluArticleBundle is not a standalone library — it is an add-on to a Sulu application (badge says Sulu >=2.0, so pick the bundle major that matches your Sulu major, e.g. ^2.x for Sulu 2, ^3.x for Sulu 3). Treat it as "pages, but for a second content entity", and everything else follows.

Minimal setup:

composer require sulu/article-bundle
// config/bundles.php
return [
    // ... Sulu core bundles first
    Sulu\Bundle\ArticleBundle\SuluArticleBundle::class => ['all' => true],
];
# config/packages/sulu_article.yaml
sulu_article:
    default_type: blog
    types:
        blog:
            translation_key: sulu_article.type_blog
        news:
            translation_key: sulu_article.type_news

If your installed version's Configuration.php exposes a templates list per type, register your template keys there too — the bundle has changed this area across majors, so read that file once before writing YAML.

bin/console doctrine:migrations:migrate   # creates the ar_* tables
bin/console cache:clear
bin/console massive:search:reindex        # admin search index

Then, in the admin:

  1. Settings → Roles — grant view/add/edit/delete/publish on the new article security contexts (sulu.articles.blog, sulu.articles.news). New types are invisible until permissions exist.
  2. Articles → Add — pick the type, pick a template, fill the localized content, publish.
  3. Open the generated URL; if it 404s you have not published for that locale or the template has no view.

Templates are XML, in the same schema as page templates, under config/templates/articles/ (typically one file per template key). Look first at an existing page template in config/templates/pages/ — article templates are 95% the same file with a different directory.

First use case to build: a blog index page (a normal Sulu page using a smart_content property pointed at the articles data source) plus the article detail rendering. That exercises the two things you will do every day: list articles and render one article.

Implementation Patterns

Model each article type as a content domain

Types are cheap: one config entry, one template folder, one security context. Use them for content that genuinely differs (blog post vs. press release vs. job ad), not for taxonomy. Taxonomy belongs in categories/tags, which articles already support and which smart content can filter on.

Treat XML templates exactly like page templates

Sections, blocks, text_line, media_selection, snippet, single_article_selection — all the page property types are available. The <view> element decides the Twig template used for the detail page, so a template key can be routed to a completely different layout without touching PHP.

<template xmlns="http://schemas.sulu.io/template/template"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd">
    <key>blog-detail</key>
    <view>pages/blog-detail</view>
    <controller>Sulu\Bundle\ArticleBundle\Controller\ArticleController::indexAction</controller>
    <cacheLifetime>604800</cacheLifetime>

    <properties>
        <property name="title" type="text_line" mandatory="true">
            <meta><title lang="en">Title</title></meta>
        </property>
        <property name="teaser" type="text_area"/>
        <property name="content" type="text_editor"/>
    </properties>
</template>

Rendering the detail page

Prefer the bundle's website controller/route provider over writing your own resolver: it takes care of locale-aware route lookup, draft/publish state and 404s. In Twig the resolved article behaves like a page structure:

{# templates/pages/blog-detail.html.twig #}
{% extends 'base.html.twig' %}

{% block content %}
    <h1>{{ article.title }}</h1>
    <p>{{ article.teaser }}</p>
    <div>{{ article.content|raw }}</div>

    <a href="{{ sulu_content_path(article.route) }}">{{ article.title }}</a>
{% endblock %}

Use sulu_content_path() (not raw path()) so the locale/domain of the current request is preserved.

Listing articles with smart content

The bundle registers an articles smart-content data source. In a page template:

<property name="articles" type="smart_content">
    <params>
        <param name="provider" value="articles"/>
        <param name="types" value="blog"/>
        <param name="limit" value="10"/>
        <param name="present_as" value="list"/>
    </params>
</property>
{% for article in content.articles %}
    <a href="{{ sulu_content_path(article.route) }}">{{ article.title }}</a>
    <time datetime="{{ article.authored|date('c') }}">{{ article.authored|date('d.m.Y') }}</time>
{% endfor %}

Always set types. With no type filter you silently mix every article type in the project into the list — usually discovered only after a client adds a second type.

Referencing single articles from pages or other articles

Use single_article_selection (or article_selection for a curated set) inside templates. Sulu resolves these to view objects automatically when you access the property from content/the structure bridge, so {% for a in content.related %} works without extra hydration. This is the standard way to build "related posts" or a manually curated homepage teaser strip.

Creating/importing articles programmatically

For bulk imports or data syncs, go through sulu_article.article_manager rather than hydrating Doctrine entities — the manager handles structure mapping, resource locators, routes and versioning:

use Sulu\Bundle\ArticleBundle\Article\ArticleManagerInterface;

final class BlogImport
{
    public function __construct(private ArticleManagerInterface $articleManager) {}

    public function import(array $row): string
    {
        $uuid = $this->articleManager->save(
            [
                'template' => 'blog-detail',
                'title'    => $row['title'],
                'teaser'   => $row['teaser'],
                'content'  => $row['body'],
            ],
            null,          // null = create
            'en',
            ['type' => 'blog']
        );

        $this->articleManager->publish($uuid, ['locale' => 'en']);

        return $uuid;
    }
}

Exact signatures have shifted between majors (save/publish options arrays in particular) — read ArticleManagerInterface in your installed version before writing the importer, and wrap it in a thin facade so an upgrade only touches one class.

Multi-locale

Articles are localized per dimension, exactly like pages: content, route, and publish state are per locale. For translations, write the target locale with the same UUID through the manager (or use locale-copy helpers if your version provides them). Do not copy the route string from another locale — the resource locator must be regenerated per locale to stay unique.

Admin/API exposure

The bundle ships admin UI, not a JSON API. For a headless front end, write a small controller and use ArticleRepository::findByUuids($uuids, $locale) / findByType() to batch-load, then serialize ArticleViewObject data yourself. Batching by UUID is the pattern that scales; per-item findByUuid in a loop is the classic N+1.

Gotchas and Tips

  • Changing templates requires a cache clear. Sulu caches structure metadata. Symptom after editing a template key or XML: old fields in the admin, or The template "x" was not found. Run cache:clear (or sulu:build) after any template change.
  • A route exists only where the article is published. Draft articles return 404 on the website and are absent from smart content lists. When a client reports "the new article is gone", check publish state and locale first, then the cache.
  • Template key vs. type mismatch is the most common setup error: the template value stored on the article must be a template that is valid for its type. Renaming a template key without a migration leaves articles pointing at a non-existent template — they still load in the admin list but blow up on edit.
  • Missing permissions look like a broken feature. After adding a type there is no role entry yet; the Articles section appears empty or forbidden. Document this in your project's setup README — it bites every new environment.
  • Resource locators are unique per entity, not globally. A page at /blog/foo and an article at /blog/foo can coexist in the routing table; one wins at runtime. If you have a page-based blog archive and article URLs under the same prefix, verify actual resolution order early.
  • Smart content is the usual performance hotspot. It resolves each item individually per locale. For heavier listings, prefer a small custom data provider or repository query with setHint/eager loading, and cache the rendered fragment instead of the query.
  • Don't touch the bundle's entity for extra fields. Put project-specific data in template properties; that keeps the schema upgradeable and gives you localization, versioning and admin UI for free. If you truly need a column, extend via your own entity mapping and expect merge pain on majors.
  • Extension points are service-level. The bundle is light on domain events; the reliable seams are decorating sulu_article.article_manager / sulu_article.article_repository, adding your own content types, and overriding the view/controller in template XML. Prefer the template controller override for one-off pages rather than replacing the bundle controller.
  • Search index is a second source of truth. After imports or bulk operations run massive:search:reindex, or articles exist but cannot be found in the admin search. Similarly, test suites need the index built after fixtures.
  • No built-in page→article conversion. If you migrate existing blog pages to articles, write a console command that reads each page structure via the structure manager and writes it through ArticleManagerInterface, preserving UUIDs in your own mapping table so redirects and cross-references survive.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager