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

Page Annotator Bundle Laravel Package

luperi/page-annotator-bundle

Page Annotator Bundle is a Symfony/PHP package for adding page annotation features to web applications. It lets users attach notes and comments to page content and gives Symfony projects a simple, configurable way to integrate annotation workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this before anything else: luperi/page-annotator-bundle is a Symfony bundle, not a Laravel package. There is no extra.laravel block, no service provider, no facade, and no config merge hook that Laravel will run for you. That single fact dictates your first move: you are either (a) working in a Symfony app, where this installs normally, or (b) working in Laravel, where "using" this package means reusing its front-end overlay and copying its persistence shape, not composer require.

If you are on Symfony, install and register it:

composer require luperi/page-annotator-bundle
// config/bundles.php
return [
    // ...
    Luperi\PageAnnotatorBundle\PageAnnotatorBundle::class => ['all' => true],
];

Then look, in this order, before writing a line of your own code:

  1. src/DependencyInjection/Configuration.php — this is the real documentation; it defines the config tree, defaults, and required keys.
  2. src/Resources/config/ — services, routes, and Twig paths; this tells you what wiring the bundle expects.
  3. Any Entity/, Model/, or Repository/ directory — this is where annotation persistence lives and whether it is Doctrine-mapped.
  4. composer.json — require and php constraints. If it wants Symfony 3/4 or PHP < 8, stop and reassess before spending more time.

If you are on Laravel (the realistic case here): do not add this to composer.json. Instead, in a 1–2 day spike, answer one question — can I extract the JS/CSS overlay assets standalone? Copy them into resources/js/vendor/page-annotator/ and build the backend yourself. A minimal annotations migration, an Annotation model, a controller, and three routes gives you an owned, testable feature in 1–3 days:

Schema::create('annotations', function (Blueprint $table) {
    $table->id();
    $table->morphs('annotatable');        // annotatable_type / annotatable_id
    $table->foreignId('user_id')->constrained();
    $table->string('page_key')->index(); // route name or URL
    $table->string('selector');          // CSS path to the target DOM node
    $table->json('position')->nullable();// x/y/w/h for region annotations
    $table->text('body');
    $table->timestamps();
});

First use case to ship: one page type (e.g. an internal QA detail page), one vertical path — user selects an element, types a note, it persists, and it re-renders for that page on reload. Do not start with multi-page, threads, or replies.

Implementation Patterns

Persistence shape. The one thing genuinely worth lifting from this bundle is the context captured per annotation: a page identifier (URL or route name), a DOM selector or coordinate pair, the author, and the body. Your Eloquent model should be polymorphic so the same table serves any reviewable resource (a report, a product draft, a client preview):

class Annotation extends Model
{
    protected $fillable = ['page_key', 'selector', 'position', 'body'];
    protected $casts = ['position' => 'array'];

    public function annotatable() { return $this->morphTo(); }
    public function user()        { return $this->belongsTo(User::class); }
}

API-first, not full-page. The overlay is client-side; the server only needs a small JSON surface. Keep it to three routes under routes/api.php:

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/annotations',   [AnnotationController::class, 'index']);   // ?page_key=...
    Route::post('/annotations',  [AnnotationController::class, 'store']);
    Route::delete('/annotations/{annotation}', [AnnotationController::class, 'destroy']);
});

index should scope by page_key and eager-load the author; store validates selector/body size and sanitizes on write; destroy is gated by a policy (author_id === $request->user()->id).

Bootstrapping the overlay in Blade. Treat the overlay as a page-scoped enhancement, not a global script. Push the script and a single JSON config blob into the layout for authenticated users only:

@auth
    @push('scripts')
        <script src="{{ asset('vendor/page-annotator/annotator.js') }}"></script>
        <script>
            PageAnnotator.init({
                pageKey: "{{ $pageKey }}",
                endpoints: { list: "/api/annotations", create: "/api/annotations" },
                csrf: "{{ csrf_token() }}",
            });
        </script>
    @endpush
@endauth

The bundle's own Twig include is the same idea — a template partial plus a page identifier — so if you are on Symfony, drop the {{ include('@PageAnnotator/...') }} tag into the base template and pass through the same identifier convention.

Authorization and tenancy. The bundle has no opinion here; you must add one. The policy is the whole security model:

class AnnotationPolicy
{
    public function delete(User $user, Annotation $a): bool
    {
        return $user->id === $a->user_id || $user->can('moderate-annotations');
    }
}

For multi-tenant apps, add a tenant_id (or scope via annotatable) and enforce it in a global scope so no controller query can leak across tenants.

Integration into an existing review flow. The most useful pattern I've seen is to attach the annotator to a "review" model that already exists (a document draft, a design mock, a UAT checklist item) and store the annotations polymorphically. That way you don't invent a new navigation surface — annotations become a side-panel on a page you already have. Emit a domain event (AnnotationCreated) so you can fan out to Slack/Jira later without touching the controller.

If you're forking the Symfony bundle into a Laravel app: strip the DI/HttpKernel coupling, remove Twig templates, replace Doctrine entities with Eloquent models, and hardcode the service construction (no container events). Keep the fork to a few hundred lines or it will rot within two upgrade cycles.

Gotchas and Tips

  • It is not installable in Laravel. No ServiceProvider, no extra.laravel, no config/*.php to publish. If a teammate says "just composer require it," they are looking at the description, not the source. Verify the bundle type in composer.json ("type": "symfony-bundle") before anything else.
  • Vintage is the top risk. Small Symfony bundles of this profile are frequently Symfony 2/3-era. Check composer.json require and php constraints in the first five minutes — a php: ">=5.6" or symfony/framework-bundle: "^3.4" constraint is a hard stop for a modern Laravel app.
  • Stored XSS is the highest-severity failure mode. Annotations capture user-supplied text and DOM/selector context that gets re-rendered to other users. Sanitize body on write (HTML Purifier or a strict allowlist) and escape on render — do not rely on the package's rendering to do it for you, and be extra suspicious of any field that claims to store "snippet" or "context."
  • Selector fragility, not breakage. Annotations keyed off CSS paths break silently when page markup changes — no exception, just orphaned comments pointing at the wrong element. Store a fallback (element text or a data attribute) and show a "position may have moved" state in the UI.
  • No upstream support. Zero maintainers-on-call, zero dependents, no test suite to inherit. Add it to your third-party register with an owner and a review cadence; otherwise it orphans in two quarters.
  • Long-lived workers (Octane/Horizon) will surface static state. If you port or fork the overlay's server-side code, audit for static/global caches; they behave differently under FPM vs Octane.
  • CSP. If you ship injected inline scripts, a strict CSP will block them. Prefer an external asset plus a nonce or data attribute over inline init blobs in production.
  • Extension point is the persistence layer, not the bundle. If the bundle ships a Doctrine AnnotationRepository interface, that's where you'd swap implementations in Symfony — but you're reimplementing it in Laravel anyway, so model the replacement on that interface so the fork's tests (if any) still make sense.
  • Debug path (Symfony, if you go that way): enable the profiler, then read var/log/dev.log for container build errors — a mis-registered bundle or missing config key fails at container compile, not runtime, and the exception message points to Configuration.php. The config tree there is the source of truth for every key the bundle accepts.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package