Props helpers
The backstage/props package exposes a props() helper backed by the backstage.props container binding. It is the parallel to core's stage() helper, meant for project-specific UX additions that should not live in core.
props(); // Backstage\Props\Helpers\Support\Helpers
props()->ping(); // example helper exposed by props ('pong')Like stage(), the props() helper also accepts a binding name (with optional dot-notation), e.g. props('something.value'), returning null when the binding is not registered.
Use stage() for everything provided by core; reach for props() only when you have registered your own helpers in the props package.
Available helpers
Out of the box, props() exposes the following helpers:
isExternalLink()
Determine whether a link points outside the current site. Relative URLs are always internal; absolute URLs are compared against the current host (falling back to config('app.url')), ignoring a www. prefix and treating subdomains as internal. Returns false for an empty link.
props()->isExternalLink('https://example.com'); // true (on a different host)
props()->isExternalLink('/about'); // false — relative URL
props()->isExternalLink('https://www.mysite.test'); // false — same host as the request<a href="{{ $url }}" @if(props()->isExternalLink($url)) target="_blank" rel="noopener noreferrer" @endif>
{{ $label }}
</a>formatCallablePhoneNumber()
Normalise a phone number for use in a tel: link: strips whitespace, converts a leading + country code to 00, and removes a (0) trunk prefix. Returns an empty string for a blank input.
props()->formatCallablePhoneNumber('+31 (0)6 1234 5678'); // '0031612345678'
props()->formatCallablePhoneNumber(null); // ''<a href="tel:{{ props()->formatCallablePhoneNumber($phone) }}">{{ $phone }}</a>Adding your own helpers
backstage/props is the package front-enders own. Add reusable Blade/template helpers here — never in backstage/cms. Every new helper is two changes inside the props package:
- A utility trait under
Helpers/Support/Utilities/containing the method. - The matching method signature on the
PropsHelperscontract.
Both are required: the trait makes the helper callable, the contract keeps props() typed and signals to other developers what is available.
1. Create a utility trait
// src/Helpers/Support/Utilities/MenuUtility.php
namespace Backstage\Props\Helpers\Support\Utilities;
use Backstage\Models\Menu;
trait MenuUtility
{
public function menu(string $slug): ?Menu
{
return Menu::where('slug', $slug)->first();
}
}The trait inherits the $app container property from DefaultUtility, so $this->app->getLocale() and $this->retrieveBinding('site') are available.
2. Update the contract
The contract is the source of truth for what props() can do. Add the method signature:
// src/Helpers/Contracts/PropsHelpers.php
namespace Backstage\Props\Helpers\Contracts;
use Backstage\Models\Menu;
interface PropsHelpers
{
public function ping(): string;
public function menu(string $slug): ?Menu;
}3. Compose the trait into Helpers
// src/Helpers/Support/Helpers.php
class Helpers implements PropsHelpers
{
use Utilities\DefaultUtility;
use Utilities\PingUtility;
use Utilities\MenuUtility; // ← new
}The helper is now available everywhere props() resolves:
{{ props()->menu('main')?->title }}$menu = props()->menu('footer');Conventions
- One trait per topic —
MenuUtility,NavigationUtility,BreadcrumbUtilityetc. KeepHelpersa one-line composition. - Always update the contract. Skipping it works at runtime but breaks IDE completion and static analysis, and makes it harder for the next front-ender to see what is available.
- Use
$this->retrieveBinding('site')(notapp('site')) when a helper needs a request-bound model, so the "outside of a content response" exception is thrown consistently. - Never add helpers to
backstage/cms. Core'sCoreHelperscontract is reserved for framework-level lookups (site(),content()); everything UX-shaped belongs in props.