Skip to main content

Creating A Custom Page

A custom page is useful when a screen does not fit the default resource CRUD flow, but you still want Qore to render backend-defined UI.

For example, imagine a project health page. It is not just a ProjectResource detail page: it combines project metadata, risk checks, a generated progress score and a small refresh action. The frontend should only mount the page; the backend should decide what the page contains.

The pattern has five pieces:

  • a frontend route;
  • a small frontend page component that renders <Node>;
  • a backend route protected by Qore auth middleware;
  • a controller that returns the root node;
  • nodes such as PageNode, CardNode, ProgressNode, ButtonNode and FlexNode that describe the UI.

Frontend Page​

The React page is intentionally small. It reads the route parameter and passes it to the backend node endpoint.

import { Node } from 'qore-next'
import { Skeleton } from 'antd'
import { useParams } from 'react-router-dom'

export default function ProjectHealthPage() {
const { projectId } = useParams()

return (
<Node
endpoint={`projects/${projectId}/health-page`}
loader={<Skeleton />}
/>
)
}

The frontend owns browser routing. The backend owns the page layout, model lookup, authorization and node behaviour.

Frontend Route​

Add the page to your authenticated app routes:

{
path: '/projects/:projectId/health',
element: <ProjectHealthPage />
}

Keep these URLs separate:

  • /projects/:projectId/health is the frontend route the user visits.
  • projects/{project}/health-page is the backend node endpoint requested by <Node>.

Backend Route​

Node endpoints should use Qore auth middleware when the page belongs to the authenticated app.

use App\Http\Controllers\ProjectHealthController;
use Illuminate\Support\Facades\Route;

use function Qore\Next\System\Helpers\qore;

Route::middleware(qore()->getAuthMiddleware())
->post('projects/{project}/health-page', ProjectHealthController::class);

Node endpoints are usually POST routes because the frontend sends node state, hidden nodes, URL data and events in the request body.

Backend Controller​

The controller builds the page from nodes and returns the root node.

namespace App\Http\Controllers;

use App\Models\Project;
use Illuminate\Routing\Controller;
use Qore\Next\System\Node\ColorType;
use Qore\Next\System\Node\Flex\FlexAlignType;
use Qore\Next\System\Node\Flex\FlexJustifyType;
use Qore\Next\System\Node\SizeType;
use Qore\Next\System\Nodes\ButtonNode;
use Qore\Next\System\Nodes\CardNode;
use Qore\Next\System\Nodes\FlexNode;
use Qore\Next\System\Nodes\PageNode;
use Qore\Next\System\Nodes\ProgressNode;
use Qore\Next\System\Nodes\TagNode;
use Qore\Next\System\Nodes\TextNode;
use Qore\Next\System\Nodes\TitleNode;

use function Qore\Next\System\Helpers\qore;

class ProjectHealthController extends Controller
{
public function __invoke(Project $project): PageNode
{
$this->authorize('view', $project);

return qore()
->page(__('common.projects.health.title'), function (PageNode $page) use ($project) {
$page->addDefaultPadding();

$page->addFlex(function (FlexNode $flex) use ($project) {
$flex->setIsVertical()
->setGap(SizeType::MIDDLE)
->addChild($this->header($project))
->addChild($this->scoreCard($project));
});
});
}

private function header(Project $project): FlexNode
{
return (new FlexNode)
->setJustify(FlexJustifyType::SPACE_BETWEEN)
->setAlign(FlexAlignType::CENTER)
->setWrap(true)
->addChild(new TitleNode($project->name, level: 3))
->addChild(
(new ButtonNode(__('common.refresh'), id: 'refresh-project-health'))
->setIcon('refresh_cw')
->onClick(function () use ($project): void {
$project->refreshHealthScore();
})
);
}

private function scoreCard(Project $project): CardNode
{
return (new CardNode(__('common.projects.health.score')))
->addChild(
(new FlexNode)
->setIsVertical()
->setGap(SizeType::SMALL)
->addChild(new ProgressNode($project->health_score))
->addChild(new TextNode(__('common.projects.health.description')))
->addChild(new TagNode($project->health_label, ColorType::GREEN))
);
}
}

Qore nodes implement Laravel's Responsable contract. Returning the PageNode lets Laravel create the NodeManager, run the node request/response lifecycle and serialize the node tree.

Why This Works Well​

The page is still a normal React route, but the backend can stay close to the model, policy, translations and workflow methods.

Use this pattern for dashboards, model-specific workflow pages, settings screens, import previews or operational pages where PHP already owns the business rules. Use a normal React page when the screen is mostly client-side interaction and only needs ordinary API endpoints.