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 a node response;
  • 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 a node response.

namespace App\Http\Controllers;

use App\Models\Project;
use Illuminate\Http\JsonResponse;
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): JsonResponse
{
$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));
});
})
->toResponse();
}

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))
);
}
}

toResponse() creates a NodeManager, runs the node request lifecycle, runs the node response lifecycle and returns the serialized 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.