litecms/contact
Contact module for LiteCMS that adds contact form handling and message storage to your Laravel application, letting you collect inquiries and manage submissions with minimal setup.
Install via Composer and rely on Laravel's package auto-discovery (LiteCMS packages ship a service provider; older ones occasionally need manual registration in config/app.php):
composer require litecms/contact
Publish assets and migrate:
php artisan vendor:publish --provider="Litecms\Contact\ContactServiceProvider"
php artisan migrate
Confirm discovery in bootstrap/cache/packages.php or php artisan about. Since this is a LiteCMS leaf module, it also registers an admin menu entry and admin routes via the core scaffolding — verify those appear under the CMS admin navigation.
Read these files first, in order:
config/contact.php — recipient address(es), mail transport hints, validation rules, spam settings. This is where you make 80% of your changes.src/Http/Controllers/ContactController.php — the actual public submission endpoint. Field names, validation, and whether mail is sent inline or queued live here.src/Models/Contact.php — fillable fields, casts, table name. Everything you build on top of the package flows through this model.resources/views/ — the default Blade form, email template, and admin list. Anything you want to change visually is an override here.routes/web.php — route names (contact.store, contact.index, etc.) you'll reference in route() helpers.First use case — a working form in minutes:
<form method="POST" action="{{ route('contact.store') }}">
@csrf
<input type="text" name="name" value="{{ old('name') }}">
<input type="email" name="email" value="{{ old('email') }}">
<textarea name="message">{{ old('message') }}</textarea>
<button type="submit">Send</button>
</form>
Always confirm the exact field names against the controller before wiring markup — LiteCMS packages inconsistently use name/email/message vs. subject/body. Submit one message end-to-end (form → DB row → admin list → notification email) before going further. That single loop tells you if mail is broken, if validation is stricter than you expect, and if the admin surface renders safely.
Override views, don't edit vendor. Publish views to resources/views/vendor/contact/ and replace the form and email templates. The default form is plain and functional — restyled to brand it, but keep the @csrf, old() repopulation, and @error blocks intact; losing old() silently degrades UX on validation failures.
Extend validation on top of the package, in a form request. If the package ships a StoreContactRequest, extend it; otherwise wrap your own. Adding fields (phone, subject, consent checkbox) means touching three places: the Blade form, the request rules, and the model's $fillable. Missing the $fillable step is the most common "why is this field not saved?" bug.
Route through the model for non-form submissions. If you also have a phone-callback widget, an API endpoint, or a CLI importer, write through Litecms\Contact\Models\Contact directly rather than hitting the HTTP endpoint. You get the same schema, observers, and admin visibility without faking a form POST or re-implementing validation.
use Litecms\Contact\Models\Contact;
Contact::create([
'name' => $lead->name,
'email' => $lead->email,
'message' => $lead->notes,
// add a `source` column if you want to distinguish lead origins
]);
Hook the notification layer, don't fight it. Add a ContactCreated event listener (or use the notification class the package dispatches) to fan out to Slack, a CRM webhook, or a ticketing system. Keeping the package as capture and store and doing routing outside its core keeps your fork surface minimal and upgradeable.
// app/Listeners/NotifySalesOfContact.php
public function handle(ContactCreated $event): void
{
Notification::route('slack', config('services.slack.leads'))
->notify(new LeadAlert($event->contact));
}
Queue the mail. If the package sends notifications inline in the request, wrap them in a queued listener or override the notification class with implements ShouldQueue. Contact traffic is low, but SMTP latency on a public endpoint causes visible timeout errors on slow transports — and a timed-out POST that did save is user-confusing.
Multi-tenant / multi-site setups. config/contact.php typically holds a single recipient. For multi-site deployments, resolve the recipient per-request from the site context and rebind at runtime:
// AppServiceProvider::boot() or a middleware
config(['contact.recipients' => $site->contact_emails]);
Confirm the package reads from config each request, not at provider boot.
Admin customization. The admin list lives in src/Http/Controllers/Admin and resources/views/admin. Adding filters, exports, or status columns (open / replied / archived) is a matter of extending the controller and Blade — but see the gotchas on authorization before you touch this.
Mail failure can lose leads. Check whether the package persists before sending. If a submission saves to the DB, fine — you can replay. If it only emails, that's a silent data-loss path and you should add an observer or a wrapper that persists first. Alert on Mail::failures() or dispatch failures.
Authorization on admin routes is a fork-level concern. Small packages frequently rely on a middleware alias that the core package wires up. A litecms/core upgrade can silently change that middleware group, leaving your submissions list exposed. Verify explicitly: hit the admin route logged out, with a public user, and with an admin account. Add an explicit can: or role: middleware if the package doesn't.
XSS on admin render. Contact forms are the classic unauthenticated-write surface. If the admin list uses {!! $contact->message !!} or renders raw HTML in the email, HTML/script can leak into your admin. Use {{ }} throughout, and if you add a "copy to clipboard" or preview pane, strip tags server-side.
Spam is your responsibility. Zero-star packages rarely ship a honeypot or rate limiting. Add all three layers yourself:
// routes/web.php
Route::post('/contact', [ContactController::class, 'store'])
->middleware(['throttle:5,1']); // 5 submissions per minute per IP
plus a honeypot field (hidden input, reject if filled), plus a timestamp check (reject sub-3-second submissions). A CAPTCHA should be last resort — it hurts conversion.
Version pinning, not constraints. With no visible maintainer cadence, pin to a commit hash or tag in composer.json and note the pin in a project README:
"litecms/contact": "1.2.3"
Then track Laravel/PHP compatibility on your own upgrade cycle. A floating ^1.2 on a single-maintainer package is how you ship a build break six months later.
Migration reversibility. Check whether php artisan migrate:rollback cleanly drops the contact tables. Small packages often ship a create-only migration without a matching down(). If that's true, back up (or archive) submissions before any migration-rollback routine on staging.
Field-name drift. Route route('contact.store') is stable; the field names are not. If you customize the form, cross-reference the request rules. A common failure is adding subject to the form while the package's request still validates a required message, giving you confusing 422s.
Testing your integration. Write a feature test that fakes mail and asserts a row is created — this is the fastest way to catch a package upgrade that changes validation or the route name:
Mail::fake();
$this->post(route('contact.store'), [
'name' => 'A', 'email' => '[email protected]', 'message' => 'Hi',
])->assertRedirect();
$this->assertDatabaseHas('contacts', ['email' => '[email protected]']);
Mail::assertSent(ContactNotification::class);
Extension points, ranked by safety:
config/contact.php) — safest, survives upgrades.resources/views/vendor/contact/) — survives unless the package renames views.ContactCreated-style events before assuming.Keep a one-page runbook. Whoever changes the recipient email, adjusts validation, or retrieves a submission during an outage will not have read the source. Three lines: where config lives, how to add a recipient, how to query submissions (Contact::latest()->paginate()). It costs a lunch break to write and pays back the first time someone else touches it.
How can I help you explore Laravel packages today?