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

Nova Html Card Laravel Package

idf/nova-html-card

idf/nova-html-card adds a Laravel Nova dashboard card that renders any HTML you give it, handy for notices, links, embeds, and lightweight widgets. Install via Composer, register the card, and pass your own markup to customize the Nova dashboard with custom content, no custom card class needed.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the package is what you think it is.

composer require idf/nova-html-card
php artisan nova:card "App\Nova\Cards\AnnouncementsCard"

Before you write anything, open vendor/idf/nova-html-card/src/HtmlCard.php. The whole package is one card class with a content(): string method and a $width property. There is no config file, no migration, no publishable assets, no service provider of its own, no routes. That ten-minute read tells you the entire surface area you will ever be working against — everything else is Nova's standard card lifecycle (cards(), canSee(), onlyOnDashboards(), $width).

The smallest thing that works.

// app/Nova/Cards/AnnouncementsCard.php
namespace App\Nova\Cards;

use Idf\NovaHtmlCard\HtmlCard;

class AnnouncementsCard extends HtmlCard
{
    public $width = '1/2';

    public function content(): string
    {
        return view('nova.cards.announcements')->render();
    }
}
{{-- resources/views/nova/cards/announcements.blade.php --}}
<div class="p-4">
    <h2 class="text-lg font-bold mb-2">{{ __('What’s new') }}</h2>
    <p class="text-sm">{{ __('Deploys are frozen Friday. See the runbook.') }}</p>
</div>
// app/Providers/NovaServiceProvider.php
public function cards(): array
{
    return [
        new \App\Nova\Cards\AnnouncementsCard,
    ];
}

Register, load /nova, done. No asset build, no npm run dev, no nova:publish.

Pick your first real use case deliberately. The highest-value first card is almost always a links/runbook card on the main dashboard: docs, Slack channel, status page, deploy runbook, changelog. It is pure static content, it has zero security surface, it validates your install end-to-end, and it is something a non-engineer will actually thank you for. Only after that works should you reach for anything dynamic.

Also verify your Nova and PHP versions match the package's composer.json constraints before you go further. Nova is closed-source and major-version-coupled; do not infer compatibility from the release date.

Implementation Patterns

Blade views, never inline HTML strings. view(...)->render() gives you route(), @auth, @lang, @foreach, view caching, and a file a reviewer can diff. A return '<div>...</div>' inside content() is a dead end the moment someone wants a second line of markup. Keep the card class a three-line wrapper and the markup in resources/views/nova/cards/.

One abstract base per visual style, thin concrete subclasses. You will end up with five notice-shaped cards; do not copy the markup five times.

abstract class NoticeCard extends HtmlCard
{
    public $width = '1/3';

    abstract protected function heading(): string;
    abstract protected function body(): string;

    public function content(): string
    {
        return view('nova.cards.notice', [
            'heading' => $this->heading(),
            'body'    => $this->body(),
        ])->render();
    }
}

class ChangelogCard extends NoticeCard
{
    protected function heading(): string { return __('Changelog'); }
    protected function body(): string    { return $this->latestRelease(); }
}

Compose a dashboard header row with $width. Three 1/3 cards — status strip, links, announcement — is the cheapest perceived-polish win in the whole package and requires no Vue work at all. Mix 1/3 and 1/2 and full-width deliberately; Nova's grid is unforgiving of a stray 1/4 that doesn't sum.

Placement is onlyOnDashboards(), visibility is canSee(). Both are Nova's, but they're the natural pairing with this card, and using them keeps your Blade free of permission conditionals.

(new ChangelogCard)->onlyOnDashboards(['main']);
(new AdminAuditLinksCard)->onlyOnDashboards(['admin']);
public function canSee(NovaRequest $request): bool
{
    return $request->user()->hasRole('admin');
}

Hidden is better than a rendered "you don't have access" shell.

Resource-page cards use the same class. Add it to a resource's cards() method rather than the dashboard to get contextual content on a detail screen:

public function cards(NovaRequest $request): array
{
    return [
        (new CustomerOnboardingCard)->onlyOnDetail(),
    ];
}

Pass context into the view by resolving the resource from the request inside content(), or build a small per-resource subclass that reads $request->resourceId.

Cache anything in content() that isn't a trivial lookup. This method runs on every dashboard request for every user who can see the card. It is an invisible DB caller. The failure mode is a dashboard that quietly becomes the slowest page in the admin panel, with the cost buried inside something labelled "an HTML card" that nobody thinks to profile.

public function content(): string
{
    return view('nova.cards.status', [
        'openTickets' => Cache::remember(
            'nova.open_tickets',
            now()->addMinutes(5),
            fn () => Ticket::whereNull('closed_at')->count()
        ),
    ])->render();
}

Changelog card keyed on filemtime(). Parsing markdown on every dashboard hit is a silly way to burn CPU:

$path = base_path('CHANGELOG.md');
$html = Cache::remember(
    'nova.changelog.' . filemtime($path),
    now()->addDay(),
    fn () => Str::markdown(File::get($path))
);

Config-driven content for anything an operator might edit. config('app.docs_url'), translation strings for announcements, etc. Content stays in version control, but changing a link doesn't require touching a Blade file.

Embedded BI widget — one per dashboard, sized explicitly. Grafana/Metabase/status pages will not size themselves:

<iframe src="{{ config('services.metabase.dashboard_url') }}"
        style="width:100%; height:480px; border:0"
        loading="lazy"></iframe>

Translate everything. Nova ships light/dark and the card content is user-facing; __('...') costs nothing and makes your announcements card reusable across locales.

Gotchas and Tips

The HTML is injected raw — that's the feature and the primary risk. Your string lands directly in the authenticated admin DOM on your highest-privilege session. Anything user-, tenant-, or third-party-API-influenced that reaches content() is stored XSS: a support-ticket subject, a display name, a webhook payload. Escape every interpolation with {{ }}. Reserve raw markup for strings you authored. If a non-engineer must author content, give them a sanitized rich-text field — never a textarea wired into this card.

CSP blanks the card with no error. Inline <script>, inline <style>, iframes, and third-party assets can all be blocked by the CSP on your Nova origin. Symptom: an empty or half-styled box, no console message you'll notice, no server-side exception. Test in staging with production CSP headers, and treat every embed as an allowlist change that goes through review.

Version 3.4.2 is an asset rebuild plus CI changes — not features. If you're on an older patch and the card registers but renders blank, upgrade first before debugging your own code. And if you maintain a "what's new" card, source it from your changelog, not this package's release feed.

Pin the version. "idf/nova-html-card": "3.4.*" and upgrade deliberately. Zero dependents means an upstream regression hits your dashboard before anyone else notices it.

Nova major upgrades are the real tax. Before any Nova bump, check this package's constraints against your actual target Nova and PHP versions in CI — do not infer compatibility from release dates. A card this small is forkable in an afternoon, but only if your subclass diff is minimal, which is another argument for the thin-wrapper pattern above.

content() must stay pure and cheap. No writes, no outbound HTTP calls, no Artisan::call(). If it reflects changing state, cache with a short TTL. If it needs to update live in front of the user, this is the wrong package.

Debugging order when the card renders blank:

  1. logger()->debug(var_export($this->content(), true)) at the top of content() — is it returning a non-empty string at all?
  2. Inspect the card in DevTools. If the wrapper exists but the inner div is empty, your Blade is throwing silently or returning ''.
  3. Check the CSP header on the response before you touch anything else.
  4. Confirm the version — see 3.4.2 above.

Dark mode will bite you. Hardcoded hex values and background: #fff look broken for half your users. Use Nova's CSS variables / Tailwind utilities and check both themes in staging. This is the single most common cosmetic complaint about hand-written card HTML.

Iframes are an unmonitored external dependency. Each one is a page-load cost on every dashboard render, plus a dead embed looks like a broken dashboard. Cap it at one per screen and make sure someone owns knowing when it goes down.

Extension points are deliberately limited — take the hint. Beyond content(), $width, canSee(), and onlyOnDashboards(), there is nothing to hook into. No config to drift, no routes to secure, no middleware to reason about. When you find yourself wanting a refresh button, a filter, or live data binding, treat that as the triage signal to escalate to a bespoke Vue card rather than bending this one.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect