Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/common/di/content-pipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'class' => ContentProcessorPipeline::class,
'__construct()' => [
Reference::to(MarkdownProcessor::class),
Reference::to(TagLinkProcessor::class),
],
],
BuildCommand::class => [
Expand Down
2 changes: 1 addition & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Enable GitHub Pages in your repository settings: go to **Settings → Pages** an

> **Tip:** Replace `X.Y.Z` with a real YiiPress version tag for reproducible, stable builds. The action builds to `_site` by default for GitHub Pages. See [GitHub Actions](github-actions.md) for custom output directories and other inputs.

For project sites such as `https://user.github.io/project/`, set `base_url` to the full deployed URL including the project path. YiiPress uses that path when rendering root-relative redirect targets, so `redirect_to: /blog/` points to `/project/blog/` in browser-facing redirect HTML. Root-relative local asset links in rendered HTML are emitted relative to each page so images and other copied assets stay valid under the same deployment path.
For project sites such as `https://user.github.io/project/`, set `base_url` to the full deployed URL including the project path. YiiPress uses that path when rendering root-relative redirect targets, so `redirect_to: /blog/` points to `/project/blog/` in browser-facing redirect HTML. Internal links generated by built-in templates and processors are emitted relative to each page, while feeds, sitemaps, canonical URLs, and redirect pages use absolute or browser-facing URLs with the configured project path. Custom templates should use the `$url()` helper for internal links and `Asset::url()` for copied assets.

> **Real-world example:** YiiPress documentation is built from the nightly binary image after the package workflow succeeds — see [`.github/workflows/build-docs.yml`](https://github.com/yiipress/engine/blob/master/.github/workflows/build-docs.yml) in this repository. Site repositories should use the release action above with a fixed version.

Expand Down
25 changes: 21 additions & 4 deletions docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ Example:
<h1><?= $h(ucfirst($taxonomyName)) ?></h1>
<ul>
<?php foreach ($terms as $term): ?>
<li><a href="/<?= $h($taxonomyName) ?>/<?= $h($term) ?>/"><?= $h($term) ?></a></li>
<li><a href="<?= $h($url($taxonomyName . '/' . $term . '/')) ?>"><?= $h($term) ?></a></li>
<?php endforeach; ?>
</ul>
```
Expand Down Expand Up @@ -324,7 +324,7 @@ Partials are reusable template fragments stored in a `partials/` subdirectory of
### Usage

```php
<?= $partial('head', ['title' => $entryTitle . ' — ' . $siteTitle]) ?>
<?= $partial('head', ['title' => $entryTitle . ' — ' . $siteTitle, 'rootPath' => $rootPath]) ?>
```

## Asset helper
Expand Down Expand Up @@ -354,12 +354,19 @@ Create a PHP file in `themes/<name>/partials/`:

```php
<?php
/** @var string $title */
/**
* @var string $title
* @var string $rootPath
* @var AssetFingerprintManifest|null $assetManifest
* @var Closure(string, int, ?string, bool): string $h
*/

use YiiPress\Build\Asset;
?>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= $h($title) ?></title>
<link rel="stylesheet" href="/assets/theme/style.css">
<link rel="stylesheet" href="<?= $h(Asset::url('assets/theme/style.css', $rootPath, $assetManifest)) ?>">
```

### Variable isolation
Expand Down Expand Up @@ -399,6 +406,16 @@ All templates receive the following helper functions as local variables:
| `$partial` | `(string $name, array $variables = []): string` | Render a partial template from the `partials/` directory |
| `$h` | `(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE, ?string $encoding = 'UTF-8', bool $doubleEncode = true): string` | Escape HTML output |
| `$t` | `(string $key, array $params = []): string` | Translate a theme UI-text key via the injected `$ui` |
| `$url` | `(string $path): string` | Build an internal site URL relative to the current output page root |

Use `$url()` for internal links generated by templates:

```php
<a href="<?= $h($url('tags/php/')) ?>">#php</a>
<a href="<?= $h($url('/')) ?>">Home</a>
```

It keeps links valid for subdirectory deployments such as GitHub Pages project sites.

Additional helpers available via static methods:

Expand Down
38 changes: 0 additions & 38 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,6 @@
<code><![CDATA[array{previous: array{title: string, url: string}|null, next: array{title: string, url: string}|null}|null]]></code>
</MixedReturnTypeCoercion>
</file>
<file src="src/Build/NotFoundPageWriter.php">
<MixedArgumentTypeCoercion>
<code><![CDATA[$params]]></code>
</MixedArgumentTypeCoercion>
<UnresolvableInclude>
<code><![CDATA[require $this->templateResolver->resolve('errors/404', $siteConfig->theme)]]></code>
</UnresolvableInclude>
</file>
<file src="src/Build/PageTemplateRenderer.php">
<MixedArgument>
<code><![CDATA[$html]]></code>
Expand Down Expand Up @@ -178,36 +170,6 @@
<code><![CDATA[(string) $token]]></code>
</RedundantCast>
</file>
<file src="src/Content/CrossReferenceResolver.php">
<InvalidNullableReturnType>
<code><![CDATA[string]]></code>
</InvalidNullableReturnType>
<NullableReturnStatement>
<code><![CDATA[preg_replace_callback(
'/\[([^\]]*)\]\(([^)#]+\.md)(#[^)]*)?\)/',
function (array $matches): string {
$text = $matches[1];
$path = $matches[2];
$fragment = $matches[3] ?? '';

$resolved = $this->resolvePath($path);
$permalink = $this->fileToPermalink[$resolved] ?? null;

if ($permalink === null) {
return $matches[0];
}

if ($this->currentPermalink !== '') {
$rootPath = RelativePathHelper::rootPath($this->currentPermalink);
$permalink = RelativePathHelper::relativize($permalink, $rootPath);
}

return '[' . $text . '](' . $permalink . $fragment . ')';
},
$markdown,
)]]></code>
</NullableReturnStatement>
</file>
<file src="src/Content/Model/Entry.php">
<RedundantCast>
<code><![CDATA[(string) $text]]></code>
Expand Down
2 changes: 1 addition & 1 deletion src/Build/Asset.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static function url(
$resolved = $assetManifest?->resolve($path) ?? $path;

if ($rootPath !== '' && $rootPath !== '/') {
return $rootPath . $resolved;
return UrlResolver::sitePath($resolved, $rootPath);
}

if ($rootPath === '/') {
Expand Down
8 changes: 4 additions & 4 deletions src/Build/AuthorPageWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,14 @@ private function writeIndexPage(
?Navigation $navigation,
bool $noWrite,
): void {
$rootPath = RelativePathHelper::rootPath('/authors/');
$rootPath = UrlResolver::rootPath('/authors/');
$uiViewData = UiViewData::forSite($siteConfig, $this->templateResolver, $siteConfig->theme);

$authorList = [];
foreach ($authors as $slug => $author) {
$authorList[] = [
'title' => $author->title,
'url' => $rootPath . 'authors/' . $slug . '/',
'url' => UrlResolver::sitePath('/authors/' . $slug . '/', $rootPath),
'avatar' => $author->avatar,
];
}
Expand Down Expand Up @@ -158,7 +158,7 @@ private function writeAuthorPage(
?Navigation $navigation,
bool $noWrite,
): void {
$rootPath = RelativePathHelper::rootPath('/authors/' . $author->slug . '/');
$rootPath = UrlResolver::rootPath('/authors/' . $author->slug . '/');
$uiViewData = UiViewData::forSite($siteConfig, $this->templateResolver, $siteConfig->theme);

$authorTitle = $author->title;
Expand All @@ -174,7 +174,7 @@ private function writeAuthorPage(
foreach ($entries as $entry) {
$collection = $collections[$entry->collection] ?? null;
$url = $collection !== null
? RelativePathHelper::relativize(PermalinkResolver::resolve($entry, $collection, $siteConfig->i18n), $rootPath)
? UrlResolver::sitePath(PermalinkResolver::resolve($entry, $collection, $siteConfig->i18n), $rootPath)
: '#';

$entryData[] = [
Expand Down
8 changes: 4 additions & 4 deletions src/Build/CollectionListingWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function write(
$currentPermalink = $pageNumber === 1
? '/' . $collection->name . '/'
: '/' . $collection->name . '/page/' . $pageNumber . '/';
$rootPath = RelativePathHelper::rootPath($currentPermalink);
$rootPath = UrlResolver::rootPath($currentPermalink);

$pagination = [
'currentPage' => $pageNumber,
Expand Down Expand Up @@ -117,7 +117,7 @@ private function renderPage(
foreach ($entries as $entry) {
$entryData[] = [
'title' => $entry->title,
'url' => RelativePathHelper::relativize(PermalinkResolver::resolve($entry, $collection, $siteConfig->i18n), $rootPath),
'url' => UrlResolver::sitePath(PermalinkResolver::resolve($entry, $collection, $siteConfig->i18n), $rootPath),
'date' => $entry->date?->format($siteConfig->dateFormat) ?? '',
'dateISO' => $entry->date?->format('Y-m-d') ?? '',
'draft' => $entry->draft,
Expand Down Expand Up @@ -147,9 +147,9 @@ private function resolvePageUrl(string $collectionName, int $pageNumber, int $to
}

if ($pageNumber === 1) {
return $rootPath . $collectionName . '/';
return UrlResolver::sitePath('/' . $collectionName . '/', $rootPath);
}

return $rootPath . $collectionName . '/page/' . $pageNumber . '/';
return UrlResolver::sitePath('/' . $collectionName . '/page/' . $pageNumber . '/', $rootPath);
}
}
10 changes: 5 additions & 5 deletions src/Build/DateArchiveWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ private function writeArchiveIndexPage(
?Navigation $navigation,
bool $noWrite,
): void {
$rootPath = RelativePathHelper::rootPath('/' . $collection->name . '/archive/');
$rootPath = UrlResolver::rootPath('/' . $collection->name . '/archive/');
$uiViewData = UiViewData::forSite($siteConfig, $this->templateResolver, $siteConfig->theme);
$html = $renderer->render('archive_index', [
'siteTitle' => $siteConfig->title,
Expand Down Expand Up @@ -179,7 +179,7 @@ private function writeYearlyPage(
?Navigation $navigation,
bool $noWrite,
): void {
$rootPath = RelativePathHelper::rootPath('/' . $collection->name . '/' . $year . '/');
$rootPath = UrlResolver::rootPath('/' . $collection->name . '/' . $year . '/');
$uiViewData = UiViewData::forSite($siteConfig, $this->templateResolver, $siteConfig->theme);

rsort($months, SORT_STRING);
Expand All @@ -188,7 +188,7 @@ private function writeYearlyPage(
foreach ($entries as $entry) {
$entryData[] = [
'title' => $entry->title,
'url' => RelativePathHelper::relativize(PermalinkResolver::resolve($entry, $collection, $siteConfig->i18n), $rootPath),
'url' => UrlResolver::sitePath(PermalinkResolver::resolve($entry, $collection, $siteConfig->i18n), $rootPath),
'date' => $entry->date?->format($siteConfig->dateFormat) ?? '',
];
}
Expand Down Expand Up @@ -234,15 +234,15 @@ private function writeMonthlyPage(
?Navigation $navigation,
bool $noWrite,
): void {
$rootPath = RelativePathHelper::rootPath('/' . $collection->name . '/' . $year . '/' . $month . '/');
$rootPath = UrlResolver::rootPath('/' . $collection->name . '/' . $year . '/' . $month . '/');
$uiViewData = UiViewData::forSite($siteConfig, $this->templateResolver, $siteConfig->theme);
$monthName = $uiViewData->ui->monthName((int) $month);

$entryData = [];
foreach ($entries as $entry) {
$entryData[] = [
'title' => $entry->title,
'url' => RelativePathHelper::relativize(PermalinkResolver::resolve($entry, $collection, $siteConfig->i18n), $rootPath),
'url' => UrlResolver::sitePath(PermalinkResolver::resolve($entry, $collection, $siteConfig->i18n), $rootPath),
'date' => $entry->date?->format($siteConfig->dateFormat) ?? '',
];
}
Expand Down
13 changes: 8 additions & 5 deletions src/Build/EntryRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function render(

$cacheContext = '';
if ($this->cache !== null) {
$cacheContext = $this->cacheContext($siteConfig, $entry, $navigation, $crossRefResolver, $navigationPager);
$cacheContext = $this->cacheContext($siteConfig, $entry, $permalink, $navigation, $crossRefResolver, $navigationPager);
$cached = $this->cache->get($entry->filePath, $cacheContext);
if ($cached !== null) {
return $this->dispatchRenderFinished($siteConfig, $entry, $permalink, $cached);
Expand All @@ -79,7 +79,8 @@ public function render(
}
$body = $resolver->resolve($body);
}
$content = $this->pipeline->process($body, $entry);
$rootPath = UrlResolver::rootPath($permalink);
$content = $this->pipeline->process($body, $entry, $rootPath);
$headAssets = $this->pipeline->collectHeadAssets($content);
$toc = $siteConfig->toc ? $this->pipeline->collectToc() : [];
$related = $this->relatedIndex?->forEntry($entry->filePath) ?? [];
Expand Down Expand Up @@ -108,12 +109,14 @@ private function dispatchRenderFinished(SiteConfig $siteConfig, Entry $entry, st
private function cacheContext(
SiteConfig $siteConfig,
Entry $entry,
string $permalink,
?Navigation $navigation,
?CrossReferenceResolver $crossRefResolver,
?array $navigationPager,
): string {
return hash('xxh128', serialize([
'siteConfig' => $siteConfig,
'permalink' => $permalink,
'navigation' => $navigation,
'navigationPager' => $navigationPager,
'assets' => $this->assetManifest?->signature() ?? '',
Expand Down Expand Up @@ -162,7 +165,7 @@ private function renderTemplate(SiteConfig $siteConfig, Entry $entry, string $co
}

$templateContext = $this->templateContexts[$themeName];
$rootPath = RelativePathHelper::rootPath($permalink);
$rootPath = UrlResolver::rootPath($permalink);
$navigationPager = $this->relativizeNavigationPager($navigationPager, $rootPath);
$lastUpdated = $this->lastUpdated($siteConfig, $entry);
$editPageUrl = $siteConfig->editPageUrl === null
Expand Down Expand Up @@ -240,7 +243,7 @@ private function entryAuthors(SiteConfig $siteConfig, Entry $entry, string $root
'slug' => $authorSlug,
'title' => $author instanceof Author ? $author->title : $authorSlug,
'url' => $siteConfig->authorPages && $author instanceof Author
? $rootPath . 'authors/' . $authorSlug . '/'
? UrlResolver::sitePath('/authors/' . $authorSlug . '/', $rootPath)
: '',
];
}
Expand Down Expand Up @@ -283,7 +286,7 @@ private function relativizeNavigationPager(?array $navigationPager, string $root

foreach (['previous', 'next'] as $key) {
if ($navigationPager[$key] !== null && str_starts_with($navigationPager[$key]['url'], '/')) {
$navigationPager[$key]['url'] = RelativePathHelper::relativize($navigationPager[$key]['url'], $rootPath);
$navigationPager[$key]['url'] = UrlResolver::sitePath($navigationPager[$key]['url'], $rootPath);
}
}

Expand Down
12 changes: 7 additions & 5 deletions src/Build/FeedGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ private function writeAtomEntry(
$xml->writeElement('summary', $summary);
}

$html = $this->renderedContent($entry);
$html = $this->renderedContent($siteConfig, $entry);
if ($html !== '') {
$xml->startElement('content');
$xml->writeAttribute('type', 'html');
Expand Down Expand Up @@ -246,7 +246,7 @@ private function writeRssItem(
$xml->writeElement('description', $summary);
}

$html = $this->renderedContent($entry);
$html = $this->renderedContent($siteConfig, $entry);
if ($html !== '') {
$xml->writeElement('content:encoded', $html);
}
Expand Down Expand Up @@ -294,13 +294,15 @@ private function createFileWriter(string $path): XMLWriter
return $xml;
}

private function renderedContent(Entry $entry): string
private function renderedContent(SiteConfig $siteConfig, Entry $entry): string
{
$cacheKey = $entry->filePath . ':' . $entry->slug;
$rootPath = UrlResolver::absoluteUrl($siteConfig, '/');
$cacheKey = $entry->filePath . ':' . $entry->slug . ':' . $rootPath;

return $this->renderedContentCache[$cacheKey] ?? ($this->renderedContentCache[$cacheKey] = $this->pipeline->process(
$entry->body(),
$entry
$entry,
$rootPath,
));
}
}
Loading
Loading