Changelog

Journal des mises à jour

Les dernières nouveautés de la plateforme, ship après ship.

v3.8.1: Torchlight Rendering Restored & CSP Local Dev Fix

Patch release fixing three regressions introduced by v3.8.0 (Security Hardening). No new features, no breaking changes.

Fixed

  • Torchlight code blocks rendered as raw text with visible backticks. The new MarkdownSanitizer was stripping all inline styles and data-* attributes from Torchlight output, killing the syntax highlighting markup. Fixed by routing Torchlight <pre> blocks around HTMLPurifier verbatim through a new purifyPreservingCodeBlocks() method, while keeping the strict policy intact for everything else. (#532)
  • Local dev environment fully broken after v3.8.0. The ContentSecurityPolicy middleware emitted CSP headers in every environment, but script-src 'self' does not cover laravel.cm.test:5173 (Vite dev server runs on a different port = different origin in CSP). Result: app.js and app.css blocked, no styles, Alpine ReferenceError cascade. CSP is now skipped in the local environment only — staging and production behavior unchanged. (#532)
  • User avatars from ui-avatars.com blocked in production by the new CSP. Domain added to img-src. (#532)

Changed

  • User resource navigation icon switched to phosphor-users-duotone for visual consistency.

Tests

7 new Pest tests in MarkdownSanitizerTest covering single block preservation, dual-theme blocks, multiple blocks per document, surrounding-content sanitization (script + onerror still stripped), and non-Torchlight <pre> defense in depth. All existing security tests still pass.

Post-deploy actions

After deploying v3.8.1 to production, regenerate the cached HTML so existing content picks up the new sanitizer behavior:

1php artisan cache:clear
2php artisan content:rerender
1php artisan cache:clear
2php artisan content:rerender

The rerender is queue-based by default; add --sync if you prefer it blocking.

Trust but verify

  • Torchlight blocks are now passed verbatim around HTMLPurifier. This is safe because (a) Torchlight escapes user input server-side via Xml::escape, (b) html_input => 'strip' in the markdown config prevents users from smuggling fake <pre><code class="torchlight"> through Markdown — only Torchlight itself can produce this output, and (c) any <pre> block that is not Torchlight still goes through the full sanitizer.
  • CSP remains strict in staging and production. The local skip is a dev ergonomics fix, not a security concession.

Full changelog: https://github.com/laravelcm/laravel.cm/compare/v3.8.0...v3.8.1

v3.8.0: Security Hardening & Markdown Pipeline Overhaul

Highlights

Security Hardening After Malicious Upload Incident

On 2026-01-09 an attacker abused an upload flaw in mckenziearts/livewire-markdown-editor v1.2 to drop a PHP webshell and a JPEG/PHP polyglot on our Scaleway S3 bucket. Although S3 itself does not execute PHP (so there was no direct RCE), the files were publicly servable under our storage domain — a phishing and malware-distribution vector. Both files have been identified, archived for forensics, and permanently removed from the bucket.

This release closes that attack surface and fixes 14 additional vulnerabilities found during the follow-up audit. Read the PR description for the full technical breakdown (#531).

New Markdown Rendering Pipeline

Every article, thread, discussion, and reply now goes through a hardened rendering pipeline:

  1. CommonMark parse with allow_unsafe_links: false and max_nesting_level: 32
  2. HTMLPurifier sanitize with a strict whitelist (no <script>, no <iframe>, no inline event handlers)
  3. MarkdownHelper liquid tag expansion (YouTube, CodePen, CodeSandbox, Giphy — from trusted PHP code, not user input)
  4. LinkFinder enrichment (external links get rel="nofollow" and target="_blank")
  5. Redis cache (7 days, keyed by SHA-256 of the markdown source)

Every model (Article, Thread, Discussion, Reply) now exposes $model->renderedBody() which reads from the new body_html column if available, or falls back to live rendering otherwise. The RenderMarkdownJob keeps the cache warm via the HasRenderedBody trait.

Content Moderation Commands

Two new Artisan commands give moderators a systematic way to scan, clean, and act on malicious content:

  • php artisan content:audit-malicious — scans all four content tables for dangerous URL extensions and accepts an optional --filenames= list for incident-specific forensics.
  • php artisan content:rerender — regenerates body_html for every record after a sanitiser update, keeping historical content protected by the latest rules.

Both commands are dry-run by default and gated behind a double confirmation prompt before any destructive operation.

Content Security Policy Enabled

The CSP middleware was previously commented out in bootstrap/app.php. It is now active with a strict allow-list covering our S3 bucket, analytics, fonts, and permitted embed providers. object-src 'none', frame-ancestors 'none', base-uri 'self', and form-action 'self' are enforced globally.

Added

  • MarkdownSanitizer service wrapping HTMLPurifier with a strict tag/attribute allow-list
  • MarkdownRenderer service with 7-day Redis SWR cache
  • SuspiciousContentDetector heuristic detector (shorteners, IP hosts, .onion, homograph lookalikes, dangerous extensions)
  • RateLimitsContentCreation trait applied to ArticleForm, ThreadForm, DiscussionForm, ReplyForm, Comments
  • HasRenderedBody model trait + RenderMarkdownJob async renderer
  • body_html and body_rendered_at columns on Article, Thread, Discussion, Reply
  • content:rerender and content:audit-malicious Artisan commands
  • ContentSecurityPolicy middleware enabled
  • 35 Pest tests (90 assertions) covering upload attacks, sanitiser, phishing detector, CSP, token hygiene

Changed

  • Bumped mckenziearts/livewire-markdown-editor to ^1.3 with strict MIME/extension/image validation
  • allow_unsafe_links set to false and max_nesting_level capped at 32 in markdown config
  • UserPolicy::ban and ::unban now receive the target user (self-ban prevention, admin protection)
  • OAuth flow now regenerates the session and scopes provider updates to the correct provider
  • Default SESSION_ENCRYPT and SESSION_SECURE_COOKIE flipped to true in .env.example
  • UserResource exposes public_id instead of the internal id

Fixed

  • Arbitrary file upload through the markdown editor on all five content entry points (CWE-434)
  • XSS through javascript: and data:text/html URIs in rendered markdown (CWE-79)
  • XSS through {!! excerpt() !!} in eight Blade views (CWE-79)
  • OAuth token stored in clear text on social_accounts (CWE-312)
  • Session fixation on OAuth callback (CWE-384)
  • NotchPay callback not idempotent — duplicate event dispatch on retry (CWE-362)
  • Unsubscribe URL not signed — forwarded emails could unsubscribe arbitrary users (CWE-639)
  • Reactions component missing authorisation, rate limit, and banned-user check
  • Internal user id exposed through UserResource

Security

  • CVSS 8.8 (CWE-434): closes arbitrary file upload via markdown editor
  • CVSS 6.1 (CWE-79): closes stored XSS vectors in excerpt rendering
  • Encrypted OAuth tokens at rest via Eloquent encrypted cast
  • Signed unsubscribe URLs with 6-month validity
  • Content Security Policy enforced globally with strict allow-list
  • Added X-Content-Type-Options: nosniff

Breaking changes

None. All additions are backwards-compatible. Legacy SocialAccount tokens remain readable (tokens are never read at runtime); new logins will persist encrypted tokens automatically.

Deployment notes

See the PR description for the full post-deploy checklist (#531). Critical steps:

  1. Run php artisan migrate --force for the new columns
  2. Rotate AWS Scaleway access keys
  3. Run php artisan content:audit-malicious --filenames=1iuZviz0Bxpb3F9ZdyYmXuCobSO9XHax1AQbP8Xf,72F15MzGP8TgCTT89TLZnxeO70aSfb2c8iDUErLv to verify the database is clean of the incident payloads
  4. Run php artisan content:rerender to backfill body_html
  5. Add SESSION_ENCRYPT=true and SESSION_SECURE_COOKIE=true to production .env

Full Changelog: https://github.com/laravelcm/laravel.cm/compare/v3.7.0...v3.8.0

v3.7.0: Public Changelog & Spotlight Improvements

Highlights

Public Changelog Page

A new /changelog page is now live on the site. It lists the 10 most recent GitHub releases in a Linear-inspired vertical timeline, with each release date sticky on the left while scrolling through a release body, and a page-level sticky sidebar on the right listing every contributor to the project since day one.

Why this matters — in most open source projects, contributors become invisible once their PR is merged. On laravel.cm, anyone who has ever contributed to the code base now gets their avatar and GitHub handle displayed on a permanent wall of fame. For early-career developers in the francophone African ecosystem, the page is a living CV backed by code.

The page auto-syncs with GitHub releases (3-to-5-day cache with stale-while-revalidate) and with project contributors (7-to-14-day cache), so each new release automatically updates the page with zero maintenance.

Accessible from:

  • Footer → Resources → Changelog
  • Spotlight command palette (Cmd+K) → type "changelog", "release", "mises à jour"

Language Switcher in the Spotlight

The command palette now exposes a Switch language command next to Toggle theme. Toggling the language:

  • Persists the choice in user settings when authenticated — follows you across all your devices
  • Falls back to session storage for guests

Both entry points (spotlight and the existing sidebar toggle) now share the same sanitisation logic.

Security Hardening

Fixed a latent open-redirect vulnerability in the locale switch handlers. url()->previous() falls back to the Referer header which is client controlled — a crafted request could have redirected visitors to an attacker-controlled domain after switching language. A new safe_previous_url() helper now enforces a strict same-origin check across every redirect target.

Added

  • Public /changelog page with Linear-inspired timeline (#529)
  • GetGithubReleasesAction and GetGithubContributorsAction with Cache::flexible SWR (#529)
  • ReleaseBodyRenderer service that hardens release bodies against XSS (strips javascript:, data:, vbscript: schemes, enforces target="_blank" rel="noopener noreferrer nofollow" on external links, linkifies PR references) (#529)
  • Per-release contributors block with stacked avatars and translated counter (#529)
  • Sticky page-level "All contributors" sidebar with flux:tooltip on hover (#529)
  • GoToChangelog Spotlight command (navigation group, synonyms in fr/en) (#529)
  • ToggleLocale Spotlight command (commands group, persists in user settings) (#529)
  • Footer link to the changelog under Resources (#529)
  • /changelog entry in sitemap.xml with lastmod pulled from the latest release, priority 0.3, changefreq monthly (#529)
  • safe_previous_url() helper in app/helpers.php, inline-documented, covered by 6 tests (#529)

Changed

  • Extracted shared GitHub API logic into AbstractGithubApiAction to remove ~60% of duplicated HTTP + error-handling code between the two actions (#529)
  • Contributor avatars now use the native avatar_url returned by the GitHub API (avatars.githubusercontent.com) instead of proxying through unavatar.io — one less external dependency (#529)
  • ReleaseData::$published_at typed as CarbonInterface instead of the concrete Carbon class, matching the rest of the codebase (#529)
  • Route /changelog rate-limited to 60 requests per minute (#529)

Fixed

  • Open-redirect through url()->previous() in ChangeLocale and ToggleLocale: the previous URL is now validated against the app host before being used as a redirect target (#529)

v3.6.0: Laravel 13 Upgrade & WebP Support

Highlights

Laravel 13 Upgrade

The application now runs on Laravel 13.5.0, bringing modern framework features and security patches.

Framework & dependencies:

  • laravel/framework^13.0
  • laravel/tinker^3.0
  • barryvdh/laravel-debugbar^4.2.6 (v3 incompatible with L13)
  • laravelcm/laravel-subscriptions^1.8.0
  • laravel-notification-channels/telegram^7.0
  • laravelcm/gamify widened to ^11|^12|^13

Laravel 13 modernization:

  • New eloquent attributes: #[Fillable], #[Hidden] replace property declarations on User and ContentIssue
  • New queue attributes: #[Timeout], #[UniqueFor] replace property declarations on GenerateNewsDigestJob
  • Middleware rename: VerifyCsrfTokenPreventRequestForgery

WebP Support for Article Covers

Article cover images are now automatically converted to WebP on upload via Spatie Media Library conversions. The WebP variant is served on article pages, listing cards, and editor previews — while the original format is preserved for external contexts (Telegram, email, RSS, Open Graph, Schema.org) where compatibility matters.

Benefits:

  • 25–35% lighter than JPEG at equivalent visual quality
  • Core Web Vitals (LCP) improvement → better SEO ranking signals
  • Lower S3 bandwidth consumption

Safe rollout: Article::getCoverImageUrl() helper falls back to the original format when the WebP conversion hasn't been generated yet, so existing articles continue to display normally during and after deployment.

⚠️ Deployment notes

Laravel 13 changes default cache key prefix and session cookie name fallbacks:

  • Cache prefix: laravelcm_cache_laravelcm-cache-
  • Session cookie: Str::slugStr::snake

If CACHE_PREFIX and SESSION_COOKIE are not explicitly set in production .env, the cache will be invalidated and active sessions logged out at deploy.

After deploy, regenerate WebP variants for existing articles:

1docker compose -f docker-compose.prod.yml exec laravelcm artisan media-library:regenerate --only=webp
1docker compose -f docker-compose.prod.yml exec laravelcm artisan media-library:regenerate --only=webp

Added

  • WebP conversion for article cover images via Spatie Media Library (#527)
  • Article::getCoverImageUrl() helper with WebP-first fallback (#527)
  • Explicit WebP acceptance in article form validation and file input (#527)

Changed

  • Upgraded to Laravel 13.5.0 with all related dependency bumps (#527)
  • Rector config now uses withComposerBased(laravel: true) for auto-loaded version sets (#527)
  • Migrated User, ContentIssue to #[Hidden] / #[Fillable] attributes (#527)
  • Migrated GenerateNewsDigestJob to #[Timeout] / #[UniqueFor] attributes (#527)
  • Typed PointType and all gamify point subclasses properties (preserves optional payee semantics) (#527)
  • Typed BaseExtension / TorchlightExtension node parameters with League\CommonMark\Node (#527)
  • Factories cleaned of phantom $attributes ?? fallbacks (#527)

Fixed

  • Unnecessary nullsafe operators removed on non-nullable User relation in YouWereMentioned notification (#527)
  • Config files now cast env() return values to string for Str::slug and explode (#527)
  • 25+ entries removed from PHPStan baseline thanks to proper type hints

v3.5.0: Article Sponsoring & Telegram Fix

Highlights

Article Sponsoring System

Admins and moderators can now sponsor articles directly from the Filament admin panel. Sponsored articles are highlighted on the homepage and display a golden "Sponsorisé" badge.

How it works:

  • Sponsor — Sets is_sponsored = true and records the sponsoring date. The article appears first on the homepage.
  • Unsponsor — Removes the article from homepage priority but preserves the sponsoring date and badge for historical reference.
  • Homepage cache is invalidated only on specific admin actions (approve, sponsor, unsponsor, delete) — not on every article edit.

Telegram Notification Fix

Fixed a bug where the Telegram notification for submitted articles was sent 4-5 times. The notification is now dispatched only on the first submission — editing an already-submitted article no longer re-triggers it. The toast message also correctly shows "Article updated" instead of "Thank you for submitting" when editing a published article.

Added

  • Sponsor/unsponsor actions in Filament admin panel with confirmation modals (#524)
  • sponsor() method in ArticlePolicy for admin/moderator authorization (#524)
  • isActivelySponsored(), activelySponsored(), sponsoredFirst() query scopes (#524)
  • "Sponsorisé" badge on article cards (homepage, listing, single post) (#524)
  • is_sponsored column in Filament articles table (#524)
  • Targeted home cache invalidation on approve, sponsor, unsponsor, and delete actions (#524)

Fixed

  • Duplicate Telegram notification sent 4-5 times on article submission (#523)
  • Wrong toast message shown when updating an already-submitted article (#523)
  • Sentinel email formatting issues (#522)

Changed

  • Homepage articles sorted by sponsoring status first, then by publication date (#524)
  • Repository setup improvements (#522)

v3.4.2: SEO Audit Fixes & Sentinel Module

Highlights

SEO Audit Fixes (Ahrefs)

Health score was 5/100 with 2,108 issues. This release addresses the critical ones:

  • Double title tags (248 pages) — Removed duplicate <title> from base layout. All pages now use a single title via archtechx/laravel-seo manager.
  • Broken images (222 references) — Created app:clean-broken-images command to remove orphaned /storage/images/ markdown references from articles, threads, discussions, and replies (pre-S3 migration leftovers).
  • Broken links (10 pages) — Fixed markdown links missing https:// prefix, dead /slack route replaced with /discord, and broken canonical URLs reset to null. Applied via data migration.
  • Non-canonical pages in sitemap (20) — Articles with external canonical URLs are now excluded from sitemap generation.

Sentinel Module

New app-modules/sentinel/ module for automated content quality monitoring. Scans content weekly for SEO issues, notifies authors by email, and auto-fixes after a 30-day deadline.

Issue types detected: missing https:// in links, failed upload placeholders (Uploading...), broken external canonical URLs (via HTTP HEAD check with GET fallback).

Lifecycle: Detected → Notified (email + 30-day deadline) → Resolved (author fixed) | AutoFixed (deadline passed)

Commands:

  • sentinel:scan — Scan content for quality issues
  • sentinel:check-canonicals — HTTP check external canonical URLs
  • sentinel:notify — Email authors grouped by user
  • sentinel:auto-fix — Fix expired issues past deadline

Scheduled weekly on Mondays at 18:00 (WAT). Models implementing Scannable: Article, Thread, Discussion, Reply.

25 Pest tests, PHPStan level 9, fr/en translations included.

Added

  • Sentinel module with Scannable interface, HasContentIssues trait, ContentIssue morphable model
  • ScanContentAction and AutoFixContentAction for detection and correction logic
  • ContentIssuesDetectedNotification queued mail notification with translations
  • CleanBrokenImageReferences command with --dry-run mode
  • Data migration to fix broken links, dead routes, and invalid canonical URLs in existing content

Changed

  • Base layout delegates title to SEO manager instead of manual <title> tag
  • GenerateArticlesSitemapCommand excludes articles with external canonical URLs
  • Rector fixes applied to Job classes (queue traits modernization)
  • PHPStan baseline updated for sitemap command signature change

Fixed

  • 248 pages with duplicate <title> tags
  • 222 broken /storage/images/ markdown image references
  • 10 internal links returning 404 (missing https, dead routes, broken canonicals)
  • 20 non-canonical pages included in sitemap

v3.4.1: Plausible Analytics

Highlights

Plausible Analytics (Self-Hosted)

Plausible Analytics is now integrated alongside Google Analytics for privacy-friendly traffic tracking. The self-hosted instance runs on analytics.universy.app and is loaded only in production via the @production directive.

Optional measurements enabled: outbound link tracking and custom events — useful for monitoring external clicks and user interactions for future sponsor media kits.

Added

  • Plausible Analytics self-hosted script in base.blade.php with outbound-links and tagged-events extensions
  • Plausible queue helper (window.plausible) for deferred custom event calls

v3.4.0: Spotlight Command Palette & Typesense Search

Highlights

Typesense Full-Text Search

Laravel Scout is now configured with the Typesense driver. Four models are indexed and searchable: Article (133 docs), Thread (89), Discussion (32), and User (634). Each model defines a toSearchableArray() with the relevant fields and a shouldBeSearchable() guard — only published articles and verified, non-banned users are indexed.

Typesense collection schemas are defined in config/scout.php with typed fields, facets on tags/channels, and query_by configuration. Search indexing runs asynchronously via a dedicated Redis queue (search), and the Docker production queue worker has been updated to listen on default,media,search.

Spotlight Command Palette (Cmd+K)

A custom command palette inspired by wire-elements/spotlight is now available across the entire site. Press Cmd+K (Mac) or Ctrl+K (Windows/Linux) to open it.

The palette has two layers: Fuse.js handles instant client-side fuzzy filtering of registered commands (navigation, actions), while Typesense powers the content search commands that drill into Articles, Forum Threads, Discussions, and Users with a breadcrumb UI.

Commands are PHP classes extending SpotlightCommand. Each declares its name, icon, group, synonyms, and an execute() method. Search commands define dependencies() for drill-down and search{Name}() methods that query Typesense.

SpotlightManager — Octane-Compatible Command Registry

Commands are managed by a SpotlightManager class registered as a scoped singleton in the container — safe for Laravel Octane. Commands are registered in AppServiceProvider with register(), registerIf(), or registerUnless(). Lookup by ID is O(1) via an indexed array.

Security Hardening

The Spotlight Livewire component enforces three layers of protection on search:

  1. Query sanitizationstrip_tags() + mb_substr() capped at 100 characters
  2. Rate limiting — 30 requests/minute keyed by authenticated user ID or IP
  3. Dependency validation — only dependency IDs declared by the command are accepted

Theme toggling validates against an allowlist and uses the existing HasSettings trait for persistence.

Added

  • Laravel Scout with Typesense driver, toSearchableArray and shouldBeSearchable on Article, Thread, Discussion, User
  • Typesense collection schemas with facets, sorting fields, and query_by config in config/scout.php
  • SpotlightCommand abstract class with kebab-case ID generation, closesAfterExecute(), grouped commands
  • SpotlightManager with register(), registerIf(), registerUnless(), getCommandById(), getVisibleCommands()
  • SpotlightSearchResult DTO with image and SpotlightResultOptions (badge label + color)
  • SpotlightCommandDependencies and SpotlightCommandDependency for drill-down search flow
  • Navigation commands: GoToArticles, GoToForum, GoToDiscussions, GoToHome, GoToAbout, GoToRules
  • Search commands: SearchArticles, SearchThreads, SearchDiscussions, SearchUsers (with avatar + XP badge)
  • ToggleTheme command with closesAfterExecute: false and user settings persistence
  • Fuse.js dependency for client-side command filtering
  • spotlight.js Alpine component with keyboard navigation, scroll-to-selected, dependency mode
  • Search trigger button in header with ⌘K hint (desktop) and magnifying glass icon (mobile)
  • Translation files lang/fr/command-palette.php and lang/en/command-palette.php
  • 22 Pest tests covering SpotlightManager and Spotlight Livewire component
  • AI coding guidelines in .ai/guidelines/ (question handling + coding rules)
  • Fuse.js npm dependency

Fixed

  • ReferrerPolicy changed from no-referrer to strict-origin-when-cross-origin — fixes YouTube embed Error 153 caused by missing Referer header
  • CacheHeaders middleware now only sets public cache headers on successful (2xx) responses — 404 pages were cached for 60s by browsers/CDN, causing published articles to remain inaccessible
  • ArticleObserver cache invalidation key aligned with SinglePost cache key format (article.{id}.{created_at_timestamp})
  • Scout queue connection reads QUEUE_CONNECTION from environment instead of hardcoded redis

Changed

  • Docker production queue worker updated to listen on default,media,search
  • phpunit.xml DB_HOST and DB_PORT removed — delegated to .env.testing for local/CI flexibility
  • Removed Searchable trait from Reply and Enterprise models (not relevant for search)
  • Old Spotlight stub files removed (app/Spotlight/*.stub)
  • /docs directory added to .gitignore

v3.3.0: AI News Digest & Docker Modernization

Highlights

AI-Powered News Digest Generation

A new Filament cpanel page allows admins to generate tech news digest articles via AI (OpenAI, Anthropic). An AI agent crawls configured RSS feeds (Laravel News, Reddit, Dev.to, etc.), analyzes the week's content, and writes structured digest articles submitted for editorial review.

The generation UI features a real-time timeline (Redis polling) tracking each step: initialization with provider/model info, source crawling with per-URL status, article saving, and completion.

Real-Time Monitoring with Flux Timeline

Generation progress displayed using Flux UI's Timeline component with collapsible steps, provider/model icons, status badges (done/failed), and leader dots. Auto-resets after completion.

Docker Compose Modernization

Replaced Laravel Sail with a custom Docker Compose setup. Production stack includes dedicated containers for the app (FrankenPHP/Octane), schedule worker, queue worker, Reverb WebSocket server, and Nightwatch agent.

Laravel Reverb & Broadcasting Infrastructure

Installed and configured Laravel Reverb for WebSocket support with dedicated production Docker container and internal HTTP communication for the broadcaster.

Added

  • Filament cpanel page for AI news digest generation with real-time monitoring
  • GenerateNewsDigestJob with structured JSON log entries in Redis
  • SaveAiGeneratedArticlesAction — unified action for AI article creation
  • NewsWriter AI agent with instructions, response parsing, and prompt builder
  • FetchRssFeed RSS/Atom parser tool for the agent
  • Telegram notification on generation completion
  • NewsDigestCacheKey enum centralizing Redis/Cache keys
  • CLI command ai:news-digest for scheduled weekly generation
  • Navigation badge on ArticleResource showing pending article count
  • SVG icons for AI providers (Anthropic, Claude, OpenAI, Gemini, DeepSeek, Ollama, xAI)
  • Predefined model selection per provider
  • Reverb container in production Docker Compose
  • Laravel Echo and Pusher JS client
  • Filament theme CSS with Flux UI and PurelineTheme integration

Fixed

  • Twitter card image showing site default instead of article image (inverted blank() condition)
  • Twitter notification self-mentioning @laravelcm when author is the platform account

Changed

  • RSS sources externalized to config/lcm.php
  • Tag create/edit switched from slideOver to modal
  • Broadcasting uses internal Docker network for server-to-server communication
  • Tag resolution optimized: single query instead of N+1
  • Redis EXPIRE calls reduced in job logging
  • Blade view pre-indexes log entries for polling performance

Removed

  • Laravel Sail dependency
  • Volt package
  • Dead NewsDigestLogUpdated broadcast event
  • phpstan-baseline.neon

v3.2.4: Telegram Notification Polish

Highlights

Richer Telegram posts for published articles

PostArticleToTelegram now sends the article cover image when one is present. Previously it dispatched a plain text message with only the title and URL. It now uses TelegramFile::photo() and includes the title, a 200-character plain-text excerpt, and a link in the caption:

1return TelegramFile::create()
2 ->to(&#39;@laravelcm&#39;)
3 ->photo($imageUrl)
4 ->content("*{$this->article->title}*\n\n_{$this->article->excerpt(200)}_\n\n{$url}");
1return TelegramFile::create()
2 ->to(&#39;@laravelcm&#39;)
3 ->photo($imageUrl)
4 ->content("*{$this->article->title}*\n\n_{$this->article->excerpt(200)}_\n\n{$url}");

Articles without a cover still fall back to a TelegramMessage.


Admin submission alerts now include author and excerpt

ArticleSubmitted — the queued notification sent to the Telegram admin channel when a new article is submitted for review — now formats its message with a structured content() method: it shows the article title, a 200-character excerpt, and a link back to the author's profile. A "Voir l'article" inline button is also attached. When a cover image exists the notification is delivered as a TelegramFile, otherwise as a TelegramMessage.


Approved articles no longer re-trigger the submission event

Editing and re-saving an already-approved article used to fire ArticleWasSubmittedForApproval again, flooding admins with redundant Telegram alerts. ArticleForm::save() now guards the event dispatch with ! $article->isApproved(), so the event fires only for articles that are genuinely awaiting a first review.


Bug Fixes

  • fix(notifications): send article cover via TelegramFile in PostArticleToTelegram when media is present by @mckenziearts in #506
  • fix(notifications): add title, excerpt and inline button to ArticleSubmitted Telegram notification by @mckenziearts in #506
  • fix(notifications): skip ArticleWasSubmittedForApproval event when article is already approved to prevent duplicate admin alerts by @mckenziearts in #506
  • fix: remove stale PHPStan baseline entries for ArticleSubmitted after notification refactor by @mckenziearts