Package: nowo-tech/blog-kit-bundle
Namespace: Nowo\BlogKitBundle
Config alias: nowo_blog_kit
Status: Baseline aligned with v1.1.0
Blog Kit Bundle provides a reusable Symfony blog domain: multilingual articles and tags, moderated comments (rate-limit and CAPTCHA strategies), optional HTML sanitizer, singleton professional settings, a secured admin CRUD UI, and public Twig rendering. Persistence is Doctrine ORM. Route access is enforced by roles or a custom checker (REQ-UI-002). Per-publication rules use security.object_access and BlogKitAccessDenied (FR-UI-003), not Symfony voters.
As a visitor, I browse published articles at /blog and open /blog/{slug} so I can read content.
Acceptance: Unpublished articles are hidden. blog_show returns 404 when the body is empty.
As a visitor, I search and filter by tag. When settings use infinite listing, scrolling loads ?partial=1 fragments.
Acceptance: Query q and tag filter the catalogue. Partial responses contain card markup only.
As a visitor, I submit a comment that waits for moderation.
Acceptance: Invalid forms flash an error. Valid comments persist as pending and do not appear until approved. Public POST is protected by configurable rate-limit and CAPTCHA strategies.
As an editor, I create, edit, publish, and delete multilingual articles and tags in /admin/blog.
Acceptance: Unauthorized users are denied. Deletes are CSRF-protected. With security.object_access.strategy: owner, editors only mutate publications they created (createdBy); canConfigure() still sees every row.
As a moderator, I approve, reject, reply to, and delete comments at /admin/blog/comments.
Acceptance: canModerate() is required. Staff replies on the public article also require canModerate().
As an administrator, I edit singleton settings at /admin/blog/settings.
Acceptance: canConfigure() is required. Listing mode, asides, card options, comment protection strategies, and HTML sanitizer strategy apply to public pages.
As an integrator, I set user_class, locales, layouts, security, listing, comment protection, and sanitizer from nowo_blog_kit.
Acceptance: Flex recipe or manual YAML boots the bundle. Twig Extra is required. Bootstrap hosts load framework CSS in layout_template / public_layout_template and may register FormKit form_themes.
As a maintainer, I run the Symfony 8 FrankenPHP demo and smoke checks.
Acceptance: make demo-smoke returns HTTP 200 from the demo home page.
| ID | Requirement |
|---|---|
| FR-BUNDLE-001 | Register services, routes, Twig namespace, and Doctrine attribute mappings |
| FR-CFG-001 | Expose a strict nowo_blog_kit configuration tree with safe defaults |
| FR-CFG-002 | Load Resources/config/services.yaml and publish container parameters |
| FR-DI-001 | Wire services with constructor injection |
| ID | Requirement |
|---|---|
| FR-ART-001 | Persist multilingual articles with translations, tags, resources, and publish flag |
| FR-ART-002 | Dispatch BlogArticlePublishedEvent when an article becomes published |
| FR-ART-003 | Public catalogue lists only published articles |
| ID | Requirement |
|---|---|
| FR-TAG-001 | Persist multilingual tags and article links |
| FR-TAG-002 | nowo:blog:sync-hashtags formats trailing hashtags and links tags |
| ID | Requirement |
|---|---|
| FR-CMT-001 | Public comments start pending |
| FR-CMT-002 | Approved comments (and approved replies) render on the article |
| FR-CMT-003 | Moderators can approve, reject, reply, and delete |
| FR-CMT-004 | Configurable comment rate-limit and CAPTCHA strategies (YAML + admin inherit) |
| ID | Requirement |
|---|---|
| FR-SET-001 | Singleton BlogSettings controls listing, asides, cards, comments, and share |
| ID | Requirement |
|---|---|
| FR-PUB-001 | Routes blog_index and blog_show render Twig under @NowoBlogKitBundle |
| FR-PUB-002 | Infinite listing fetches partial=1 HTML and appends cards |
| ID | Requirement |
|---|---|
| FR-ADM-001 | Admin CRUD for articles, tags, comments, and settings |
| FR-UI-001 | Configurable CSS framework and layout templates |
| FR-UI-002 | Admin routes require Symfony Security or a custom checker unless allow_unauthenticated |
| FR-UI-003 | Object-level publication access via security.object_access (none / owner / host service) and BlogKitAccessDenied — not Symfony voters |
| FR-FORM-001 | Prepend FormKit profiles blog_kit / filter and type_map.entity when the host has not defined them |
| FR-TWIG-001 | Application templates under templates/bundles/NowoBlogKitBundle/ take precedence |
| FR-TWIG-005 | Kit Twig must not ship raw <form> / <input>; render with form_start + form_row loops |
| ID | Requirement |
|---|---|
| FR-ORM-001 | Doctrine attribute mapping; paginated admin and public queries; optional table prefix |
| ID | Requirement |
|---|---|
| FR-I18N-002 | Ship catalogues for en, es, fr, de, it, nl, pt with key parity |
| FR-I18N-003 | Translation domain is NowoBlogKitBundle |
| ID | Requirement |
|---|---|
| FR-SEC-005 | Optional article HTML sanitizer (none / strip / allowlist / host service) on persist and public render |
| ID | Requirement |
|---|---|
| FR-DEMO-001 | FrankenPHP Symfony 8 demo boots and returns HTTP 200 |
| FR-DEMO-002 | Demo admin chrome is a host admin/layout.html.twig with Bootstrap 5, Icons, FormKit profiles, and bootstrap_5_layout |
- Host authentication / user management (UserKit / AuthKit remain host-owned)
- Visual page builder (see PageLayoutKitBundle)