Skip to content

Dual Responses

Dual responses allow your controllers to serve both Hyper requests and traditional HTTP requests from a single method. This progressive enhancement approach ensures your application works with or without JavaScript, providing a better user experience and improved accessibility.

The web() Method

The web() method sets a fallback response for non-Hyper requests:

php
public function index()
{
    $contacts = Contact::latest()->paginate(10);

    return hyper()
        ->fragment('contacts.index', 'contact-list', compact('contacts'))
        ->web(view('contacts.index', compact('contacts')));
}

How it works:

  • Hyper request (has Datastar-Request header) → Returns the fragment
  • Traditional request (initial page load, direct URL) → Returns the full view
  • Same data, different rendering - Both responses use the same $contacts data

Why Use Dual Responses?

Progressive Enhancement

Your application works without JavaScript enabled:

php
public function search()
{
    $query = request('q');
    $results = Product::search($query)->paginate(20);

    return hyper()
        ->fragment('search.results', 'results-list', compact('results', 'query'))
        ->web(view('search.results', compact('results', 'query')));
}
  • With JavaScript: Fast fragment updates, no page reload
  • Without JavaScript: Traditional page navigation still works
  • Same codebase: No duplicate controller logic

SEO Benefits

Search engines can crawl your full pages:

php
public function show($slug)
{
    $post = Post::where('slug', $slug)->firstOrFail();

    return hyper()
        ->fragment('posts.show', 'post-content', compact('post'))
        ->web(view('posts.show', compact('post')));
}

Initial page loads serve complete HTML with proper meta tags, titles, and structured data that search engines can index.

Graceful Degradation

If the Hyper JavaScript fails to load or errors occur, traditional navigation continues working:

php
public function filter()
{
    $category = request('category');
    $products = Product::where('category_id', $category)->paginate(20);

    return hyper()
        ->fragment('products.index', 'product-grid', compact('products'))
        ->web(view('products.index', compact('products')));
}

Users on slow connections or with JavaScript disabled still get a functional application.

Implementation Patterns

Basic Pattern

The most common dual response pattern:

php
public function index()
{
    $data = $this->getData();

    return hyper()
        ->fragment('view.name', 'fragment-name', $data)
        ->web(view('view.name', $data));
}

Both responses use identical data but render differently based on the request type.

With Fragments in Views

Structure your Blade views to support both modes:

blade
{{-- resources/views/products/index.blade.php --}}
<!DOCTYPE html>
<html>
<head>
    <title>Products</title>
    @hyper
</head>
<body>
    <div class="container">
        <h1>Products</h1>

        @fragment('product-grid')
        <div id="product-grid">
            <div class="grid">
                @foreach($products as $product)
                    <x-product-card :product="$product" />
                @endforeach
            </div>

            <div class="pagination">
                {{ $products->links() }}
            </div>
        </div>
        @endfragment
    </div>
</body>
</html>
php
public function index()
{
    $products = Product::paginate(20);

    return hyper()
        ->fragment('products.index', 'product-grid', compact('products'))
        ->web(view('products.index', compact('products')));
}
  • Hyper request: Only the fragment content is rendered
  • Web request: Complete page with header, footer, and layout
  • Single template: No code duplication

Multiple Fragments

Update multiple sections for Hyper, full page otherwise:

php
public function dashboard()
{
    $stats = $this->getStats();
    $activities = $this->getActivities();
    $notifications = $this->getNotifications();

    return hyper()
        ->fragments([
            ['view' => 'dashboard', 'fragment' => 'stats', 'data' => compact('stats')],
            ['view' => 'dashboard', 'fragment' => 'activities', 'data' => compact('activities')]
        ])
        ->web(view('dashboard', compact('stats', 'activities', 'notifications')));
}

Handling Different Data Needs

Sometimes Hyper and web requests need different data:

php
public function dashboard()
{
    // Data needed by both
    $stats = $this->getStats();

    if (request()->isHyper()) {
        // Hyper: Just update stats
        return hyper()->fragment('dashboard', 'stats', compact('stats'));
    }

    // Web: Need full page data
    $activities = $this->getActivities();
    $notifications = $this->getNotifications();
    $user = auth()->user();

    return view('dashboard', compact('stats', 'activities', 'notifications', 'user'));
}

Or use conditional loading:

php
public function show($id)
{
    $product = Product::findOrFail($id);

    $response = hyper()->fragment('products.show', 'product-details', compact('product'));

    // Only load related products for full page views
    if (!request()->isHyper()) {
        $related = Product::related($product)->take(6)->get();
        $response->web(view('products.show', compact('product', 'related')));
    }

    return $response;
}

Learn More