Skip to content

Luna: Routing & Visits

Introduction

In a Luna application there is only one router — LaraGram's. You never define client-side routes. Instead, you navigate by telling Luna to visit a URL: it issues an XHR, receives the new page object, swaps the component, and updates the browser history. This chapter covers the two ways to trigger a visit — the <Link> component and the programmatic router — plus redirects, prefetching, polling, and history management.

Defining Routes

Routes are ordinary LaraGram routes. Any route whose controller returns a Luna response is a Luna page. For pages that need no controller logic, the Route::luna() macro is a shortcut:

php
use LaraGram\Support\Facades\Route;

Route::luna('/about', 'About');
Route::luna('/pricing', 'Pricing', ['plans' => $plans]);

Route::luna($uri, $component, $props) registers a GET/HEAD route that renders $component with static $props. For anything dynamic, use a normal controller route.

The <Link> component renders an anchor that performs a Luna visit instead of a full page load. Import it from your framework adapter:

jsx
import { Link } from '@laraxgram/react'

<Link href="/users">Users</Link>
<Link href="/users/1">View user</Link>

By default <Link> issues a GET visit. Change the method and send data for non-GET actions:

jsx
<Link href="/logout" method="post" as="button">Log out</Link>
<Link href="/users/1" method="delete" as="button">Delete</Link>

NOTE

When method is anything other than get, render the link as="button" — an anchor performing a destructive action is a browser accessibility and prefetch hazard.

<Link> accepts the full range of visit options as props:

PropDescription
hrefThe target URL (or a { url, method } pair).
methodHTTP verb: get (default), post, put, patch, delete.
dataData to send with the request.
only / exceptPerform a partial reload of specific props.
preserveScrollKeep the current scroll position after the visit.
preserveStateKeep the current page component's local state.
replaceReplace the current history entry instead of pushing a new one.
headersExtra request headers.
asyncFire the visit without blocking navigation.
prefetchPrefetch the page (true, 'hover', 'mount', 'click').
cacheForHow long a prefetched response stays fresh.
jsx
<Link
    href="/users"
    only={['users']}
    preserveScroll
    prefetch="hover"
>
    Users
</Link>

The Vue and Svelte adapters export an equivalent <Link> component with the same options (bound the idiomatic way for each framework).

Manual Visits

Import the router to navigate programmatically — from event handlers, effects, or anywhere in your app:

js
import { router } from '@laraxgram/react'

router.visit('/users')
router.get('/users', { page: 2 })
router.post('/users', { name: 'Ada' })
router.put('/users/1', { name: 'Ada' })
router.patch('/users/1', { name: 'Ada' })
router.delete('/users/1')

router.visit(url, options) is the general form; get/post/put/patch/delete are convenience wrappers. All accept the same options object:

js
router.post('/users', data, {
    only: ['users', 'flash'],   // Partial reload.
    preserveScroll: true,
    preserveState: true,
    replace: false,
    headers: { 'X-Custom': '1' },
    onBefore: (visit) => { /* return false to cancel */ },
    onStart: (visit) => {},
    onProgress: (event) => {},   // Upload progress.
    onSuccess: (page) => {},
    onError: (errors) => {},     // Validation errors.
    onFinish: (visit) => {},
    onCancel: () => {},
})

Reloading the Current Page

router.reload() re-requests the current URL — commonly with only for a partial reload:

js
router.reload()
router.reload({ only: ['notifications'] })

Preserving Scroll & State

By default a visit resets scroll to the top and replaces the page component (destroying its local state). Opt out with preserveScroll and preserveState. Both accept a boolean or a callback that decides based on the response:

js
router.get('/search', { q }, {
    preserveScroll: true,
    preserveState: true,   // Keep the search box focused & filled.
})

preserveState defaults to true for non-GET visits so form fields survive a validation error.

Redirects

Server-side redirects work exactly as they do in any LaraGram app — return a redirect and Luna follows it, swapping to the destination page:

php
public function store(Request $request)
{
    User::create($request->validated());

    return redirect()->route('users.index');
}

Luna automatically converts a redirect after a PUT/PATCH/DELETE into a GET for the destination (via the bundled EnsureGetOnRedirect middleware), so you never get a stale method on the follow-up visit.

External Redirects

To send the user to a non-Luna URL (an external site, or a route outside your SPA), use Luna::location(). It responds with a header that tells the client to perform a full browser navigation:

php
return Luna::location('https://billing.example.com/checkout');

The luna_location() helper is a shorthand for the same thing.

The Page Object

Every page component can read the current page object — its props, URL, component name, and version — via usePage:

jsx
import { usePage } from '@laraxgram/react'

function Nav() {
    const { auth, url } = usePage().props
    return <span>{auth.user.name}</span>
}

Vue exposes usePage() from @laraxgram/vue3; Svelte exposes both usePage() and a page store from @laraxgram/svelte. This is how you access shared props — including the Telegram context — anywhere in the tree.

Prefetching

Prefetching fetches a page's data before the user navigates, so the visit feels instant. Enable it per-link with the prefetch prop:

jsx
<Link href="/users" prefetch>Users</Link>              {/* on hover (default) */}
<Link href="/users" prefetch="mount" cacheFor="30s">…</Link>
<Link href="/users" prefetch={['hover', 'click']}>…</Link>
StrategyWhen the fetch fires
hoverWhen the pointer hovers the link (default when prefetch is true).
mountAs soon as the link mounts.
clickOn mouse-down, slightly ahead of the click.

cacheFor controls how long a prefetched response is considered fresh (e.g. '30s', '5m', or milliseconds). Prefetch programmatically with router.prefetch(url, options, { cacheFor }), and clear the cache with router.flushAll().

The usePrefetch() hook exposes the prefetch state for a given set of options — isPrefetching, isPrefetched, lastUpdatedAt, and a flush() — so you can reflect it in the UI.

Polling

To keep a page fresh, poll the server on an interval. The usePoll hook wraps a repeating partial reload:

jsx
import { usePoll } from '@laraxgram/react'

// Re-fetch `notifications` every 5 seconds.
const { stop, start } = usePoll(5000, { only: ['notifications'] })

Pass { keepAlive: true } to keep polling while the tab is in the background, and { autoStart: false } to start it manually via the returned start(). Under the hood this is router.poll(interval, options), which you can also call directly for non-component code. Vue and Svelte export the same usePoll helper.

Infinite Scroll

The <InfiniteScroll> component pairs with merge props or scroll props to load more items as the user reaches the end of a list — no manual page tracking:

jsx
import { InfiniteScroll } from '@laraxgram/react'

<InfiniteScroll data="posts">
    {posts.map((post) => <PostCard key={post.id} post={post} />)}
</InfiniteScroll>

Each time the sentinel scrolls into view, Luna requests the next page and merges it into the posts prop. The core also exports a headless useInfiniteScroll for custom UIs. Vue and Svelte export the same component.

Remembering Local State

Luna can persist component-local state (form drafts, scroll positions, open panels) in the browser history, so it is restored when the user navigates back. Use the framework's remember hook:

jsx
import { useRemember } from '@laraxgram/react'

// Restored automatically on back-navigation, keyed by 'filters'.
const [filters, setFilters] = useRemember({ search: '' }, 'filters')

For lower-level control, router.remember(data, key) and router.restore(key) read and write the history state directly. Vue and Svelte export useRemember as well.

Global Events

Subscribe to the visit lifecycle globally — useful for progress bars, analytics, or logging. router.on() returns an unsubscribe function:

js
const off = router.on('start', (event) => {
    console.log('Visiting', event.detail.visit.url.href)
})

// Later:
off()

Available events include before, start, progress, success, error, invalid, exception, finish, navigate, prefetching, and prefetched.

History Encryption

Page data is stored in the browser's history state so back/forward navigation is instant. For sensitive pages you can encrypt that data so it is unreadable after logout. Enable it globally in config/luna.php (history.encrypt), per-response, or via middleware:

php
// Per-response.
return Luna::render('Account', $props)->encryptHistory();

// Clear all encrypted history (e.g. on logout).
Luna::clearHistory();

Or apply the luna.encrypt middleware to a route group. See config/luna.php for the global switch.

Next Steps

With navigation covered, move on to sending data to the server with Forms, or wiring up your framework and layouts in Frontend Setup.

Released under the MIT License.