anomaly/partials-module
Anomaly Partials Module adds reusable content partials to PyroCMS/Streams Platform, letting you create, manage, and render shared snippets across pages and layouts. It integrates with the admin UI and Twig for consistent content reuse.
The partials module is a PyroCMS / Streams Platform add-on, not a standalone Laravel package. Before anything else, confirm your project already depends on anomaly/streams-platform (typically via pyrocms/pyrocms or a hand-assembled Pyro install). Installing it into a generic Laravel app will pull in the full Streams stack and fail without the surrounding admin/addon plumbing.
Installation path:
composer require anomaly/partials-module
Laravel's package auto-discovery registers Anomaly\PartialsModule\PartialsModuleServiceProvider. If you've disabled discovery, add it to config/app.php providers.
Then run the module migrations. On Pyro, this is the standard php artisan migrate. The migration registers a partials stream (the content model — fields like name, slug, content) rather than creating a conventional MySQL table, so the schema lives in the Streams streams, stream_fields, stream_assignments, and entry_partials tables. If migrations no-op, verify streams_platform migrations ran first.
First use case: log into the admin panel, navigate to Content → Partials (/admin/partials), create a partial named "Promo Banner" with slug promo-banner and a paragraph of HTML content in the rich editor.
Then in any Twig view (Pyro uses Twig for themes and addon views):
{{ partial('promo-banner') }}
That is the whole loop — create in admin, render by slug in a template. If partial() is undefined in your Twig environment, check that the plugin is registered (php artisan addon:reinstall anomaly.partials_module or clear the addon cache) and that you're not running a version of the module mismatched against your Streams core.
Where to look first in the repo:
src/PartialsModuleServiceProvider.php — what gets registered (plugins, field types, routes, view overrides).src/Partial/PartialModel.php and src/Partial/PartialRepository.php — the entry model and the query surface (findBySlug, find, etc.).src/Partial/Plugin/*.php — the Twig/Laravel plugin exposing the partial() function. This defines the real public API signature (what arguments it accepts — slug, params, context).src/Partial/FieldType/PartialsFieldType.php — the field type that lets other streams (pages, blog posts) reference partials.migrations/ — the stream definition; the source of truth for which fields exist.Slug rendering with parameters. The plugin's render method typically accepts a params array that is merged into the partial's view data. The most common pattern for dynamic banner/user-greeting content:
{{ partial('welcome-banner', { user_name: entry.author.name, cta_url: url('pricing') }) }}
Inside the partial's stored content, reference those keys as Twig variables. Keep parameter names snake_case or camelCase consistently — the merge is literal, there is no validation layer, and typos silently produce blanks rather than errors.
Referencing partials from other entries (field type). Install the partials field type and add a "Partials" field to a Pages page type, blog stream, or any stream. That gives editors a dropdown/picker in the admin to select a partial, and the resulting entry value is a PartialModel (or a collection, if the field allows multiple). In your template, call it directly:
{{ entry.my_partial_field.render()|raw }}
or, if you prefer the slug-based plugin:
{{ partial(entry.my_partial_field.slug) }}
Prefer the model's render() here — it keeps you from re-querying by slug and avoids a stale-cache lookup when the entry was just saved.
Programmatic access (Blade, mail, jobs). Partials are just Streams entries, so anywhere in PHP:
use Anomaly\PartialsModule\Partial\PartialModel;
$partial = app(PartialModel::class)->findBySlug('promo-banner');
echo $partial->render(); // HTML string
// or grab the raw stored content
echo $partial->content;
Inject the model into mailables and notifications to keep promotional copy out of hard-coded views. Because findBySlug hits the Streams repository, wrap hot paths in a cache:
echo Cache::rememberForever("partial:promo-banner", fn() =>
app(PartialModel::class)->findBySlug('promo-banner')?->render()
);
Flush that key from a PartialEvents::SAVED/DELETED listener rather than TTL-ing it.
Embedding inside the rich editor (editor integration). The module pairs with the Streams editor field type. If a partial has been assigned to a field, editors can embed a reference to another partial's slug inside page content and have it be resolved at render time via the plugin. This is the "snippet" use case Pyro docs advertise — a footer disclaimer, a legal block, a recurring CTA — that a dozen pages share and one editor updates once.
Layout/footer/nav slots. The highest-leverage day-to-day pattern: replace hard-coded blocks in your theme's layout with partials:
{# resources/views/layouts/base.twig #}
<footer>{{ partial('site-footer') }}</footer>
{{ partial('cookie-consent') }}
These survive theme redeploys, since the content lives in the database rather than the theme directory.
Multi-variant rendering. Because parameters are passed in, the same partial can render differently per context:
{{ partial('cta-button', { label: 'Start free trial', style: 'primary' }) }}
{{ partial('cta-button', { label: 'Talk to sales', style: 'ghost' }) }}
Inside the partial, branch on style. This gives you a poor man's component library without writing a Blade component for each.
Localization. Streams supports translated fields; if you need per-locale versions of a partial, add translations via the Streams UI and set the locale on the request. Test with the admin locale switcher before assuming it works — translation coverage of the content field depends on whether the field was created as translatable in the migration.
Ecosystem lock-in is the big one. Dependents: 0 and a score of 0 are not red flags about quality — they reflect that this lives inside the PyroCMS monorepo ecosystem and is almost never installed standalone. Budget time for the entire anomaly/* dependency graph; you cannot use this without Pyro or a hand-built Streams install, and you cannot easily rip it out later without rewriting templates.
Version-alignment failures are the most common install blocker. All anomaly/* packages must share a compatible major version (e.g., all ^3.0). Mixing a ^2.0 partials module with a ^3.0 streams platform typically manifests as a boot error from the service provider or a "table not found" for streams_streams tables — not a clean dependency conflict. Pin versions in composer.json and upgrade the whole anomaly stack together.
Auto-escaping will surprise you. If your Twig environment has autoescape on and the plugin does not return a Twig\Markup / Illuminate\Support\HtmlString, {{ partial('x') }} prints escaped HTML. Either the plugin marks its result safe or you need |raw. Check the plugin class to confirm which; if you need |raw in one template, use it everywhere, and if the plugin already marks safe, adding |raw is harmless but misleading. For user-submitted partial content, do not assume it is sanitized — pass it through Purifier or the Streams sanitizer before rendering.
Streams caches the schema, not just data. Deleting or renaming a field in the admin updates the stream definition, but templates referencing entry.old_field will silently render empty until you clear the Streams schema cache (php artisan cache:clear and sometimes streams:compile). When a field "disappears" from output, this is almost always the cause.
Slugs are unique and rarely re-generated. The slug field is unique across partials. Changing the display name in the admin does not update the slug after creation (deliberate, to avoid breaking templates). A duplicate slug in a seeder will throw a database unique constraint on entry_partials.slug — handle it with updateOrCreate semantics on the Streams repository, not raw inserts.
Field type requires the fields module. The partials field type only registers if anomaly/fields-module and the base field types are present. If it is missing from the field-type dropdown, check installed addons rather than assuming the module is broken.
Rich editor is a separate install. The content field is only a WYSIWYG if anomaly/editor-field-type (and its chosen editor driver, e.g. anomaly/wysiwyg-field-type / Redactor) is installed. Otherwise it degrades to a plain textarea — a very common "why does the editor look broken" report.
Do not put {{ partial() }} inside loops without caching. Each call hits the Streams entry repository. A footer partial rendered inside a list of 200 products will run 200 queries unless Streams' repository-level caching catches it. Hoist the call out of the loop and pass the pre-rendered variable, or wrap in Cache::remember.
Debugging a "renders blank" partial. In order: (1) confirm findBySlug('slug') returns a model in tinker — a null model returns nothing and Twig does not complain; (2) confirm the partial is not enabled = false (Streams entries have a enabled flag; the plugin may filter disabled ones); (3) confirm the content field is populated and the field assignment has not been detached from the stream; (4) check whether the partial uses parameters that your call site omitted.
Extension points are the reason to use this rather than a Blade include. Listen to Anomaly\PartialsModule\Partial\Event\PartialWasSaved (or the Streams EntryWasSaved) to rebuild an HTML cache, regenerate a sitemap, or push content to a CDN. Override the default view by placing resources/views/partials/partial.twig in your theme — useful for wrapping every partial in a standard <section> tag without editing each one. And the entry model is the place to add accessor methods (e.g., a thumbnail() accessor that resolves an image field to a URL) so templates stay clean.
No first-class test harness. The package ships without meaningful test coverage; if you write integration tests against it, build a fixture Pyro install or a trait that seeds the partials stream. Mock PartialModel::render() in unit tests and cover the plugin's argument-passing behavior with a thin smoke test — that's where regressions between minor versions tend to land.
How can I help you explore Laravel packages today?