julienmonnerie/kirby/src/Cms/Page.php

1325 lines
29 KiB
PHP

<?php
namespace Kirby\Cms;
use Closure;
use Kirby\Content\Field;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Filesystem\Dir;
use Kirby\Http\Response;
use Kirby\Http\Uri;
use Kirby\Panel\Page as Panel;
use Kirby\Template\Template;
use Kirby\Toolkit\A;
use Kirby\Toolkit\LazyValue;
use Kirby\Toolkit\Str;
use Throwable;
/**
* The `$page` object is the heart and
* soul of Kirby. It is used to construct
* pages and all their dependencies like
* children, files, content, etc.
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Page extends ModelWithContent
{
use HasChildren;
use HasFiles;
use HasMethods;
use HasSiblings;
use PageActions;
use PageSiblings;
public const CLASS_ALIAS = 'page';
/**
* All registered page methods
* @todo Remove when support for PHP 8.2 is dropped
*/
public static array $methods = [];
/**
* Registry with all Page models
*/
public static array $models = [];
/**
* The PageBlueprint object
*/
protected PageBlueprint|null $blueprint = null;
/**
* Nesting level
*/
protected int $depth;
/**
* Sorting number + slug
*/
protected string|null $dirname;
/**
* Path of dirnames
*/
protected string|null $diruri = null;
/**
* Draft status flag
*/
protected bool $isDraft;
/**
* The Page id
*/
protected string|null $id = null;
/**
* The template, that should be loaded
* if it exists
*/
protected Template|null $intendedTemplate = null;
protected array|null $inventory = null;
/**
* The sorting number
*/
protected int|null $num;
/**
* The parent page
*/
protected Page|null $parent;
/**
* Absolute path to the page directory
*/
protected string|null $root;
/**
* The URL-appendix aka slug
*/
protected string $slug;
/**
* The intended page template
*/
protected Template|null $template = null;
/**
* The page url
*/
protected string|null $url;
/**
* Creates a new page object
*/
public function __construct(array $props)
{
if (isset($props['slug']) === false) {
throw new InvalidArgumentException('The page slug is required');
}
parent::__construct($props);
$this->slug = $props['slug'];
// Sets the dirname manually, which works
// more reliable in connection with the inventory
// than computing the dirname afterwards
$this->dirname = $props['dirname'] ?? null;
$this->isDraft = $props['isDraft'] ?? false;
$this->num = $props['num'] ?? null;
$this->parent = $props['parent'] ?? null;
$this->root = $props['root'] ?? null;
$this->setBlueprint($props['blueprint'] ?? null);
$this->setChildren($props['children'] ?? null);
$this->setDrafts($props['drafts'] ?? null);
$this->setFiles($props['files'] ?? null);
$this->setTemplate($props['template'] ?? null);
$this->setUrl($props['url'] ?? null);
}
/**
* Magic caller
*/
public function __call(string $method, array $arguments = []): mixed
{
// public property access
if (isset($this->$method) === true) {
return $this->$method;
}
// page methods
if ($this->hasMethod($method)) {
return $this->callMethod($method, $arguments);
}
// return page content otherwise
return $this->content()->get($method);
}
/**
* Improved `var_dump` output
* @codeCoverageIgnore
*/
public function __debugInfo(): array
{
return array_merge($this->toArray(), [
'content' => $this->content(),
'children' => $this->children(),
'siblings' => $this->siblings(),
'translations' => $this->translations(),
'files' => $this->files(),
]);
}
/**
* Returns the url to the api endpoint
* @internal
*/
public function apiUrl(bool $relative = false): string
{
if ($relative === true) {
return 'pages/' . $this->panel()->id();
}
return $this->kirby()->url('api') . '/pages/' . $this->panel()->id();
}
/**
* Returns the blueprint object
*/
public function blueprint(): PageBlueprint
{
return $this->blueprint ??= PageBlueprint::factory(
'pages/' . $this->intendedTemplate(),
'pages/default',
$this
);
}
/**
* Returns an array with all blueprints that are available for the page
*/
public function blueprints(string|null $inSection = null): array
{
if ($inSection !== null) {
return $this->blueprint()->section($inSection)->blueprints();
}
if ($this->blueprints !== null) {
return $this->blueprints;
}
$blueprints = [];
$templates = $this->blueprint()->changeTemplate() ?? $this->blueprint()->options()['changeTemplate'] ?? [];
$currentTemplate = $this->intendedTemplate()->name();
if (is_array($templates) === false) {
$templates = [];
}
// add the current template to the array if it's not already there
if (in_array($currentTemplate, $templates) === false) {
array_unshift($templates, $currentTemplate);
}
// make sure every template is only included once
$templates = array_unique($templates);
foreach ($templates as $template) {
try {
$props = Blueprint::load('pages/' . $template);
$blueprints[] = [
'name' => basename($props['name']),
'title' => $props['title'],
];
} catch (Exception) {
// skip invalid blueprints
}
}
return $this->blueprints = array_values($blueprints);
}
/**
* Builds the cache id for the page
*/
protected function cacheId(string $contentType): string
{
$cacheId = [$this->id()];
if ($this->kirby()->multilang() === true) {
$cacheId[] = $this->kirby()->language()->code();
}
$cacheId[] = $contentType;
return implode('.', $cacheId);
}
/**
* Prepares the content for the write method
* @internal
*/
public function contentFileData(
array $data,
string|null $languageCode = null
): array {
return A::prepend($data, [
'title' => $data['title'] ?? null,
'slug' => $data['slug'] ?? null
]);
}
/**
* Returns the content text file
* which is found by the inventory method
* @internal
* @deprecated 4.0.0
* @todo Remove in v5
* @codeCoverageIgnore
*/
public function contentFileName(string|null $languageCode = null): string
{
Helpers::deprecated('The internal $model->contentFileName() method has been deprecated. Please let us know via a GitHub issue if you need this method and tell us your use case.', 'model-content-file');
return $this->intendedTemplate()->name();
}
/**
* Call the page controller
* @internal
*
* @throws \Kirby\Exception\InvalidArgumentException If the controller returns invalid objects for `kirby`, `site`, `pages` or `page`
*/
public function controller(
array $data = [],
string $contentType = 'html'
): array {
// create the template data
$data = array_merge($data, [
'kirby' => $kirby = $this->kirby(),
'site' => $site = $this->site(),
'pages' => new LazyValue(fn () => $site->children()),
'page' => new LazyValue(fn () => $site->visit($this))
]);
// call the template controller if there's one.
$controllerData = $kirby->controller(
$this->template()->name(),
$data,
$contentType
);
// merge controller data with original data safely
// to provide original data to template even if
// it wasn't returned by the controller explicitly
if (empty($controllerData) === false) {
$classes = [
'kirby' => App::class,
'site' => Site::class,
'pages' => Pages::class,
'page' => Page::class
];
foreach ($controllerData as $key => $value) {
$data[$key] = match (true) {
// original data wasn't overwritten
array_key_exists($key, $classes) === false => $value,
// original data was overwritten, but matches expected type
$value instanceof $classes[$key] => $value,
// throw error if data was overwritten with wrong type
default => throw new InvalidArgumentException('The returned variable "' . $key . '" from the controller "' . $this->template()->name() . '" is not of the required type "' . $classes[$key] . '"')
};
}
}
// unwrap remaining lazy values in data
// (happens if the controller didn't override an original lazy Kirby object)
$data = LazyValue::unwrap($data);
return $data;
}
/**
* Returns a number indicating how deep the page
* is nested within the content folder
*/
public function depth(): int
{
return $this->depth ??= (substr_count($this->id(), '/') + 1);
}
/**
* Returns the directory name (UID with optional sorting number)
*/
public function dirname(): string
{
if ($this->dirname !== null) {
return $this->dirname;
}
if ($this->num() !== null) {
return $this->dirname = $this->num() . Dir::$numSeparator . $this->uid();
}
return $this->dirname = $this->uid();
}
/**
* Returns the directory path relative to the `content` root
* (including optional sorting numbers and draft directories)
*/
public function diruri(): string
{
if (is_string($this->diruri) === true) {
return $this->diruri;
}
if ($this->isDraft() === true) {
$dirname = '_drafts/' . $this->dirname();
} else {
$dirname = $this->dirname();
}
if ($parent = $this->parent()) {
return $this->diruri = $parent->diruri() . '/' . $dirname;
}
return $this->diruri = $dirname;
}
/**
* Checks if the page exists on disk
*/
public function exists(): bool
{
return is_dir($this->root()) === true;
}
/**
* Constructs a Page object and also
* takes page models into account.
* @internal
*/
public static function factory($props): static
{
return static::model($props['model'] ?? 'default', $props);
}
/**
* Redirects to this page,
* wrapper for the `go()` helper
*
* @since 3.4.0
*
* @param array $options Options for `Kirby\Http\Uri` to create URL parts
* @param int $code HTTP status code
*/
public function go(array $options = [], int $code = 302): void
{
Response::go($this->url($options), $code);
}
/**
* Checks if the intended template
* for the page exists.
*/
public function hasTemplate(): bool
{
return $this->intendedTemplate() === $this->template();
}
/**
* Returns the Page Id
*/
public function id(): string
{
if ($this->id !== null) {
return $this->id;
}
// set the id, depending on the parent
if ($parent = $this->parent()) {
return $this->id = $parent->id() . '/' . $this->uid();
}
return $this->id = $this->uid();
}
/**
* Returns the template that should be
* loaded if it exists.
*/
public function intendedTemplate(): Template
{
if ($this->intendedTemplate !== null) {
return $this->intendedTemplate;
}
return $this->setTemplate($this->inventory()['template'])->intendedTemplate();
}
/**
* Returns the inventory of files
* children and content files
* @internal
*/
public function inventory(): array
{
if ($this->inventory !== null) {
return $this->inventory;
}
$kirby = $this->kirby();
return $this->inventory = Dir::inventory(
$this->root(),
$kirby->contentExtension(),
$kirby->contentIgnore(),
$kirby->multilang()
);
}
/**
* Compares the current object with the given page object
*
* @param \Kirby\Cms\Page|string $page
*/
public function is($page): bool
{
if ($page instanceof self === false) {
if (is_string($page) === false) {
return false;
}
$page = $this->kirby()->page($page);
}
if ($page instanceof self === false) {
return false;
}
return $this->id() === $page->id();
}
/**
* Checks if the page is accessible that accessible and listable.
* This permission depends on the `read` option until v5
*/
public function isAccessible(): bool
{
// TODO: remove this check when `read` option deprecated in v5
if ($this->isReadable() === false) {
return false;
}
static $accessible = [];
$role = $this->kirby()->user()?->role()->id() ?? '__none__';
$template = $this->intendedTemplate()->name();
$accessible[$role] ??= [];
return $accessible[$role][$template] ??= $this->permissions()->can('access');
}
/**
* Checks if the page is the current page
*/
public function isActive(): bool
{
return $this->site()->page()?->is($this) === true;
}
/**
* Checks if the page is a direct or indirect ancestor
* of the given $page object
*/
public function isAncestorOf(Page $child): bool
{
return $child->parents()->has($this->id()) === true;
}
/**
* Checks if the page can be cached in the
* pages cache. This will also check if one
* of the ignore rules from the config kick in.
*/
public function isCacheable(): bool
{
$kirby = $this->kirby();
$cache = $kirby->cache('pages');
$options = $cache->options();
$ignore = $options['ignore'] ?? null;
// the pages cache is switched off
if (($options['active'] ?? false) === false) {
return false;
}
// inspect the current request
$request = $kirby->request();
// disable the pages cache for any request types but GET or HEAD
if (in_array($request->method(), ['GET', 'HEAD']) === false) {
return false;
}
// disable the pages cache when there's request data
if (empty($request->data()) === false) {
return false;
}
// disable the pages cache when there are any params
if ($request->params()->isNotEmpty()) {
return false;
}
// check for a custom ignore rule
if ($ignore instanceof Closure) {
if ($ignore($this) === true) {
return false;
}
}
// ignore pages by id
if (is_array($ignore) === true) {
if (in_array($this->id(), $ignore) === true) {
return false;
}
}
return true;
}
/**
* Checks if the page is a child of the given page
*
* @param \Kirby\Cms\Page|string $parent
*/
public function isChildOf($parent): bool
{
return $this->parent()?->is($parent) ?? false;
}
/**
* Checks if the page is a descendant of the given page
*
* @param \Kirby\Cms\Page|string $parent
*/
public function isDescendantOf($parent): bool
{
if (is_string($parent) === true) {
$parent = $this->site()->find($parent);
}
if (!$parent) {
return false;
}
return $this->parents()->has($parent->id()) === true;
}
/**
* Checks if the page is a descendant of the currently active page
*/
public function isDescendantOfActive(): bool
{
if ($active = $this->site()->page()) {
return $this->isDescendantOf($active);
}
return false;
}
/**
* Checks if the current page is a draft
*/
public function isDraft(): bool
{
return $this->isDraft;
}
/**
* Checks if the page is the error page
*/
public function isErrorPage(): bool
{
return $this->id() === $this->site()->errorPageId();
}
/**
* Checks if the page is the home page
*/
public function isHomePage(): bool
{
return $this->id() === $this->site()->homePageId();
}
/**
* It's often required to check for the
* home and error page to stop certain
* actions. That's why there's a shortcut.
*/
public function isHomeOrErrorPage(): bool
{
return $this->isHomePage() === true || $this->isErrorPage() === true;
}
/**
* Check if the page can be listable by the current user
* This permission depends on the `read` option until v5
*/
public function isListable(): bool
{
// TODO: remove this check when `read` option deprecated in v5
if ($this->isReadable() === false) {
return false;
}
// not accessible also means not listable
if ($this->isAccessible() === false) {
return false;
}
static $listable = [];
$role = $this->kirby()->user()?->role()->id() ?? '__none__';
$template = $this->intendedTemplate()->name();
$listable[$role] ??= [];
return $listable[$role][$template] ??= $this->permissions()->can('list');
}
/**
* Checks if the page has a sorting number
*/
public function isListed(): bool
{
return $this->isPublished() && $this->num() !== null;
}
public function isMovableTo(Page|Site $parent): bool
{
try {
return PageRules::move($this, $parent);
} catch (Throwable) {
return false;
}
}
/**
* Checks if the page is open.
* Open pages are either the current one
* or descendants of the current one.
*/
public function isOpen(): bool
{
if ($this->isActive() === true) {
return true;
}
if ($this->site()->page()?->parents()->has($this->id()) === true) {
return true;
}
return false;
}
/**
* Checks if the page is not a draft.
*/
public function isPublished(): bool
{
return $this->isDraft() === false;
}
/**
* Check if the page can be read by the current user
* @todo Deprecate `read` option in v5 and make the necessary changes for `access` and `list` options.
*/
public function isReadable(): bool
{
static $readable = [];
$role = $this->kirby()->user()?->role()->id() ?? '__none__';
$template = $this->intendedTemplate()->name();
$readable[$role] ??= [];
return $readable[$role][$template] ??= $this->permissions()->can('read');
}
/**
* Checks if the page is sortable
*/
public function isSortable(): bool
{
return $this->permissions()->can('sort');
}
/**
* Checks if the page has no sorting number
*/
public function isUnlisted(): bool
{
return $this->isPublished() && $this->num() === null;
}
/**
* Checks if the page access is verified.
* This is only used for drafts so far.
* @internal
*/
public function isVerified(string|null $token = null): bool
{
if (
$this->isPublished() === true &&
$this->parents()->findBy('status', 'draft') === null
) {
return true;
}
if ($token === null) {
return false;
}
return $this->token() === $token;
}
/**
* Returns the root to the media folder for the page
* @internal
*/
public function mediaRoot(): string
{
return $this->kirby()->root('media') . '/pages/' . $this->id();
}
/**
* The page's base URL for any files
* @internal
*/
public function mediaUrl(): string
{
return $this->kirby()->url('media') . '/pages/' . $this->id();
}
/**
* Creates a page model if it has been registered
* @internal
*/
public static function model(string $name, array $props = []): static
{
$class = static::$models[$name] ?? null;
$class ??= static::$models['default'] ?? null;
if ($class !== null) {
$object = new $class($props);
if ($object instanceof self) {
return $object;
}
}
return new static($props);
}
/**
* Returns the last modification date of the page
*/
public function modified(
string|null $format = null,
string|null $handler = null,
string|null $languageCode = null
): int|string|false|null {
$identifier = $this->isDraft() === true ? 'changes' : 'published';
$modified = $this->storage()->modified(
$identifier,
$languageCode
);
if ($modified === null) {
return null;
}
return Str::date($modified, $format, $handler);
}
/**
* Returns the sorting number
*/
public function num(): int|null
{
return $this->num;
}
/**
* Returns the panel info object
*/
public function panel(): Panel
{
return new Panel($this);
}
/**
* Returns the parent Page object
*/
public function parent(): Page|null
{
return $this->parent;
}
/**
* Returns the parent id, if a parent exists
* @internal
*/
public function parentId(): string|null
{
return $this->parent()?->id();
}
/**
* Returns the parent model,
* which can either be another Page
* or the Site
* @internal
*/
public function parentModel(): Page|Site
{
return $this->parent() ?? $this->site();
}
/**
* Returns a list of all parents and their parents recursively
*/
public function parents(): Pages
{
$parents = new Pages();
$page = $this->parent();
while ($page !== null) {
$parents->append($page->id(), $page);
$page = $page->parent();
}
return $parents;
}
/**
* Return the permanent URL to the page using its UUID
* @since 3.8.0
*/
public function permalink(): string|null
{
return $this->uuid()?->url();
}
/**
* Returns the permissions object for this page
*/
public function permissions(): PagePermissions
{
return new PagePermissions($this);
}
/**
* Draft preview Url
* @internal
*/
public function previewUrl(): string|null
{
$preview = $this->blueprint()->preview();
if ($preview === false) {
return null;
}
$url = match ($preview) {
true => $this->url(),
default => $preview
};
if ($this->isDraft() === true) {
$uri = new Uri($url);
$uri->query->token = $this->token();
$url = $uri->toString();
}
return $url;
}
/**
* Renders the page with the given data.
*
* An optional content type can be passed to
* render a content representation instead of
* the default template.
*
* @param string $contentType
* @throws \Kirby\Exception\NotFoundException If the default template cannot be found
*/
public function render(array $data = [], $contentType = 'html'): string
{
$kirby = $this->kirby();
$cache = $cacheId = $html = null;
// try to get the page from cache
if (empty($data) === true && $this->isCacheable() === true) {
$cache = $kirby->cache('pages');
$cacheId = $this->cacheId($contentType);
$result = $cache->get($cacheId);
$html = $result['html'] ?? null;
$response = $result['response'] ?? [];
$usesAuth = $result['usesAuth'] ?? false;
$usesCookies = $result['usesCookies'] ?? [];
// if the request contains dynamic data that the cached response
// relied on, don't use the cache to allow dynamic code to run
if (Responder::isPrivate($usesAuth, $usesCookies) === true) {
$html = null;
}
// reconstruct the response configuration
if (empty($html) === false && empty($response) === false) {
$kirby->response()->fromArray($response);
}
}
// fetch the page regularly
if ($html === null) {
if ($contentType === 'html') {
$template = $this->template();
} else {
$template = $this->representation($contentType);
}
if ($template->exists() === false) {
throw new NotFoundException([
'key' => 'template.default.notFound'
]);
}
$kirby->data = $this->controller($data, $contentType);
// trigger before hook and apply for `data`
$kirby->data = $kirby->apply('page.render:before', [
'contentType' => $contentType,
'data' => $kirby->data,
'page' => $this
], 'data');
// render the page
$html = $template->render($kirby->data);
// trigger after hook and apply for `html`
$html = $kirby->apply('page.render:after', [
'contentType' => $contentType,
'data' => $kirby->data,
'html' => $html,
'page' => $this
], 'html');
// cache the result
$response = $kirby->response();
if ($cache !== null && $response->cache() === true) {
$cache->set($cacheId, [
'html' => $html,
'response' => $response->toArray(),
'usesAuth' => $response->usesAuth(),
'usesCookies' => $response->usesCookies(),
], $response->expires() ?? 0);
}
}
return $html;
}
/**
* @internal
* @throws \Kirby\Exception\NotFoundException If the content representation cannot be found
*/
public function representation(mixed $type): Template
{
$kirby = $this->kirby();
$template = $this->template();
$representation = $kirby->template($template->name(), $type);
if ($representation->exists() === true) {
return $representation;
}
throw new NotFoundException('The content representation cannot be found');
}
/**
* Returns the absolute root to the page directory
* No matter if it exists or not.
*/
public function root(): string
{
return $this->root ??= $this->kirby()->root('content') . '/' . $this->diruri();
}
/**
* Returns the PageRules class instance
* which is being used in various methods
* to check for valid actions and input.
*/
protected function rules(): PageRules
{
return new PageRules();
}
/**
* Search all pages within the current page
*/
public function search(string|null $query = null, string|array $params = []): Pages
{
return $this->index()->search($query, $params);
}
/**
* Sets the Blueprint object
*
* @return $this
*/
protected function setBlueprint(array|null $blueprint = null): static
{
if ($blueprint !== null) {
$blueprint['model'] = $this;
$this->blueprint = new PageBlueprint($blueprint);
}
return $this;
}
/**
* Sets the intended template
*
* @return $this
*/
protected function setTemplate(string|null $template = null): static
{
if ($template !== null) {
$this->intendedTemplate = $this->kirby()->template($template);
}
return $this;
}
/**
* Sets the Url
*
* @return $this
*/
protected function setUrl(string|null $url = null): static
{
if (is_string($url) === true) {
$url = rtrim($url, '/');
}
$this->url = $url;
return $this;
}
/**
* Returns the slug of the page
*/
public function slug(string|null $languageCode = null): string
{
if ($this->kirby()->multilang() === true) {
$languageCode ??= $this->kirby()->languageCode();
$defaultLanguageCode = $this->kirby()->defaultLanguage()->code();
if (
$languageCode !== $defaultLanguageCode &&
$translation = $this->translations()->find($languageCode)
) {
return $translation->slug() ?? $this->slug;
}
}
return $this->slug;
}
/**
* Returns the page status, which
* can be `draft`, `listed` or `unlisted`
*/
public function status(): string
{
if ($this->isDraft() === true) {
return 'draft';
}
if ($this->isUnlisted() === true) {
return 'unlisted';
}
return 'listed';
}
/**
* Returns the final template
*/
public function template(): Template
{
if ($this->template !== null) {
return $this->template;
}
$intended = $this->intendedTemplate();
if ($intended->exists() === true) {
return $this->template = $intended;
}
return $this->template = $this->kirby()->template('default');
}
/**
* Returns the title field or the slug as fallback
*/
public function title(): Field
{
return $this->content()->get('title')->or($this->slug());
}
/**
* Converts the most important
* properties to array
*/
public function toArray(): array
{
return array_merge(parent::toArray(), [
'children' => $this->children()->keys(),
'files' => $this->files()->keys(),
'id' => $this->id(),
'mediaUrl' => $this->mediaUrl(),
'mediaRoot' => $this->mediaRoot(),
'num' => $this->num(),
'parent' => $this->parent()?->id(),
'slug' => $this->slug(),
'template' => $this->template(),
'uid' => $this->uid(),
'uri' => $this->uri(),
'url' => $this->url()
]);
}
/**
* Returns a verification token, which
* is used for the draft authentication
*/
protected function token(): string
{
return $this->kirby()->contentToken(
$this,
$this->id() . $this->template()
);
}
/**
* Returns the UID of the page.
* The UID is basically the same as the
* slug, but stays the same on
* multi-language sites. Whereas the slug
* can be translated.
*
* @see self::slug()
*/
public function uid(): string
{
return $this->slug;
}
/**
* The uri is the same as the id, except
* that it will be translated in multi-language setups
*/
public function uri(string|null $languageCode = null): string
{
// set the id, depending on the parent
if ($parent = $this->parent()) {
return $parent->uri($languageCode) . '/' . $this->slug($languageCode);
}
return $this->slug($languageCode);
}
/**
* Returns the Url
*
* @param array|string|null $options
*/
public function url($options = null): string
{
if ($this->kirby()->multilang() === true) {
if (is_string($options) === true) {
return $this->urlForLanguage($options);
}
return $this->urlForLanguage(null, $options);
}
if ($options !== null) {
return Url::to($this->url(), $options);
}
if (is_string($this->url) === true) {
return $this->url;
}
if ($this->isHomePage() === true) {
return $this->url = $this->site()->url();
}
if ($parent = $this->parent()) {
if ($parent->isHomePage() === true) {
return $this->url = $this->kirby()->url('base') . '/' . $parent->uid() . '/' . $this->uid();
}
return $this->url = $this->parent()->url() . '/' . $this->uid();
}
return $this->url = $this->kirby()->url('base') . '/' . $this->uid();
}
/**
* Builds the Url for a specific language
*
* @internal
* @param string|null $language
*/
public function urlForLanguage(
$language = null,
array|null $options = null
): string {
if ($options !== null) {
return Url::to($this->urlForLanguage($language), $options);
}
if ($this->isHomePage() === true) {
return $this->url = $this->site()->urlForLanguage($language);
}
if ($parent = $this->parent()) {
if ($parent->isHomePage() === true) {
return $this->url = $this->site()->urlForLanguage($language) . '/' . $parent->slug($language) . '/' . $this->slug($language);
}
return $this->url = $this->parent()->urlForLanguage($language) . '/' . $this->slug($language);
}
return $this->url = $this->site()->urlForLanguage($language) . '/' . $this->slug($language);
}
}