Skip to content
Open
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
20 changes: 20 additions & 0 deletions docs/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,31 @@ content/
│ │ └── john-doe.svg
│ ├── john-doe.md
│ └── jane-smith.md
├── data/ # Site data files exposed to templates
│ └── company.yaml
├── config.yaml # Site-wide settings (see docs/configuration.md)
├── navigation.yaml # Menu definitions
└── standalone-page.md # Standalone page (not in any collection)
```

## Site Data

Put YAML files in `content/data/` to expose structured site data to templates. Each `.yaml` or `.yml` file becomes one key in the `$data` template variable using the file name without the extension.

For example, `content/data/company.yaml`:

```yaml
name: Acme
links:
- /about/
```

is available in templates as:

```php
<?= $h($data['company']['name']) ?>
```

## Collections

First-level directories under `content/` are **collections** (e.g., `blog/`, `docs/`).
Expand Down
2 changes: 2 additions & 0 deletions docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ All built-in page templates receive these additional variables:
| `$uiLanguages` | `list<string>` | Available UI languages exposed by the site |
| `$uiCatalogs` | `array<string, array<string, string>>` | Theme UI catalogs for client-side switching |
| `$ui` | `YiiPress\I18n\UiText` | Injected localized UI-text helper for bundled theme labels |
| `$data` | `array<string, mixed>` | Site data loaded from `content/data/*.yaml` |
| `$h` | `Closure(string, int, ?string, bool): string` | Injected alias for `htmlspecialchars()` |
| `$t` | `Closure(string, array): string` | Injected shortcut for `$ui->get()` in templates |

Expand All @@ -119,6 +120,7 @@ Example:
```php
<html lang="<?= $h($language) ?>">
<button aria-label="<?= $h($t('search')) ?>">
<span><?= $h($data['company']['name'] ?? '') ?></span>
```

In the bundled `minimal` theme, `$language` is the content language of the current page, while the remembered UI language can differ and is applied client-side after load.
Expand Down
1 change: 1 addition & 0 deletions roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
- [x] Theme system — installable/distributable themes
- [x] Template partials/includes support
- [x] Template helper functions documentation
- [x] Site data files exposed to templates (`content/data/*.yaml` as `$data`)
- [x] Multiple layout support (per-entry layout selection via front matter)
- [x] Beautiful default theme
- [x] VuePress-style documentation layout with left sidebar navigation and right document table of contents
Expand Down
2 changes: 2 additions & 0 deletions src/Build/AuthorPageWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ private function writeIndexPage(

$html = $renderer->render('author_index', [
'siteTitle' => $siteConfig->title,
'data' => $siteConfig->data,
'authorList' => $authorList,
'nav' => $navigation,
'rootPath' => $rootPath,
Expand Down Expand Up @@ -198,6 +199,7 @@ private function writeAuthorPage(

$html = $renderer->render('author', [
'siteTitle' => $siteConfig->title,
'data' => $siteConfig->data,
'authorTitle' => $authorTitle,
'authorEmail' => $authorEmail,
'authorUrl' => $authorUrl,
Expand Down
1 change: 1 addition & 0 deletions src/Build/CollectionListingWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ private function renderPage(

return $renderer->render('collection_listing', [
'siteTitle' => $siteConfig->title,
'data' => $siteConfig->data,
'collectionTitle' => $collection->title,
'collectionName' => $collection->name,
'entries' => $entryData,
Expand Down
3 changes: 3 additions & 0 deletions src/Build/DateArchiveWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ private function writeArchiveIndexPage(
$uiViewData = UiViewData::forSite($siteConfig, $this->templateResolver, $siteConfig->theme);
$html = $renderer->render('archive_index', [
'siteTitle' => $siteConfig->title,
'data' => $siteConfig->data,
'collectionName' => $collection->name,
'collectionTitle' => $collection->title,
'years' => $years,
Expand Down Expand Up @@ -197,6 +198,7 @@ private function writeYearlyPage(

$html = $renderer->render('archive_yearly', [
'siteTitle' => $siteConfig->title,
'data' => $siteConfig->data,
'collectionName' => $collection->name,
'collectionTitle' => $collection->title,
'year' => $year,
Expand Down Expand Up @@ -251,6 +253,7 @@ private function writeMonthlyPage(

$html = $renderer->render('archive_monthly', [
'siteTitle' => $siteConfig->title,
'data' => $siteConfig->data,
'collectionName' => $collection->name,
'collectionTitle' => $collection->title,
'year' => $year,
Expand Down
1 change: 1 addition & 0 deletions src/Build/EntryRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ private function renderTemplate(SiteConfig $siteConfig, Entry $entry, string $co
$uiViewData = UiViewData::forSite($siteConfig, $this->templateResolver, $themeName);
$variables = [
'siteTitle' => $siteConfig->title,
'data' => $siteConfig->data,
'entryTitle' => $entry->title,
'content' => $content,
'date' => $entry->date?->format($siteConfig->dateFormat) ?? '',
Expand Down
1 change: 1 addition & 0 deletions src/Build/NotFoundPageWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function write(SiteConfig $siteConfig, string $outputDir, ?Navigation $na
$renderer = new PageTemplateRenderer($this->templateResolver, $siteConfig->theme, $this->assetManifest, $siteConfig->minify);
$html = $renderer->render('errors/404', [
'siteTitle' => $siteConfig->title,
'data' => $siteConfig->data,
'nav' => $navigation,
'rootPath' => $rootPath,
'search' => $siteConfig->search !== null,
Expand Down
2 changes: 2 additions & 0 deletions src/Build/TaxonomyPageWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ private function writeIndexPage(
$taxonomyLabel = $uiViewData->ui->taxonomyLabel($taxonomyName);
$html = $renderer->render('taxonomy_index', [
'siteTitle' => $siteConfig->title,
'data' => $siteConfig->data,
'taxonomyName' => $taxonomyName,
'terms' => $terms,
'nav' => $navigation,
Expand Down Expand Up @@ -126,6 +127,7 @@ private function writeTermPage(

$html = $renderer->render('taxonomy_term', [
'siteTitle' => $siteConfig->title,
'data' => $siteConfig->data,
'taxonomyName' => $taxonomyName,
'term' => $term,
'entries' => $entries,
Expand Down
29 changes: 28 additions & 1 deletion src/Console/BuildCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -1284,7 +1284,12 @@ private function collectSourceInventory(string $contentDir, array $assetSourceFi
/** @var SplFileInfo $item */
if ($item->isDir()) {
$name = $item->getFilename();
if ($name === 'assets' || $name === 'authors' || $name === 'templates') {
if ($name === 'assets' || $name === 'authors' || $name === 'templates' || $name === 'data') {
if ($name === 'data') {
foreach ($this->collectDataFiles($item->getPathname()) as $dataFile) {
$configFiles[] = $dataFile;
}
}
continue;
}

Expand Down Expand Up @@ -1322,6 +1327,28 @@ private function collectSourceInventory(string $contentDir, array $assetSourceFi
];
}

/**
* @return list<string>
*/
private function collectDataFiles(string $dataDir): array
{
$files = [];
$iterator = new FilesystemIterator($dataDir, BaseFilesystemIterator::SKIP_DOTS);
foreach ($iterator as $item) {
/** @var SplFileInfo $item */
if ($item->isDir()) {
continue;
}

$extension = strtolower($item->getExtension());
if ($extension === 'yaml' || $extension === 'yml') {
$files[] = $item->getPathname();
}
}

return $files;
}

/**
* @param array{entry: Entry, filePath: string, permalink: string, sourcePath: string} $task
* @return array{entry: Entry, filePath: string, permalink: string, sourcePath: string, navigationPager?: array{previous: array{title: string, url: string}|null, next: array{title: string, url: string}|null}|null}
Expand Down
2 changes: 2 additions & 0 deletions src/Content/Model/SiteConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/**
* @param list<string> $taxonomies
* @param array<string, mixed> $params
* @param array<string, mixed> $data
*/
public function __construct(
public string $title,
Expand Down Expand Up @@ -38,5 +39,6 @@ public function __construct(
public ?string $reportIssueUrl = null,
public bool $authorPages = false,
public bool $minify = true,
public array $data = [],
) {}
}
4 changes: 2 additions & 2 deletions src/Content/Parser/ContentParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public function parseCollections(string $contentDir): array
}

$name = $item->getFilename();
if ($name === 'assets' || $name === 'authors') {
if ($name === 'assets' || $name === 'authors' || $name === 'data') {
continue;
}

Expand Down Expand Up @@ -153,7 +153,7 @@ public function parseAllEntries(string $contentDir): Generator
}

$name = $item->getFilename();
if ($name === 'assets' || $name === 'authors') {
if ($name === 'assets' || $name === 'authors' || $name === 'data') {
continue;
}

Expand Down
5 changes: 5 additions & 0 deletions src/Content/Parser/SiteConfigParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
use YiiPress\Content\Model\SiteConfig;

use function array_is_list;
use function basename;
use function dirname;
use function file_get_contents;
use function implode;
use function is_array;
Expand Down Expand Up @@ -89,6 +91,9 @@ public function parse(string $filePath): SiteConfig
reportIssueUrl: self::parseOptionalString($data['report_issue'] ?? null),
authorPages: (bool) ($data['author_pages'] ?? false),
minify: (bool) ($data['minify'] ?? true),
data: basename($filePath) === 'config.yaml'
? (new SiteDataParser())->parse(dirname($filePath) . '/data')
: [],
);
}

Expand Down
68 changes: 68 additions & 0 deletions src/Content/Parser/SiteDataParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace YiiPress\Content\Parser;

use FilesystemIterator;
use SplFileInfo;

use function basename;
use function file_get_contents;
use function is_dir;
use function ksort;
use function strtolower;
use function yaml_parse;

final class SiteDataParser
{
/**
* @return array<string, mixed>
*/
public function parse(string $dataDir): array
{
if (!is_dir($dataDir)) {
return [];
}

$data = [];
$iterator = new FilesystemIterator($dataDir, FilesystemIterator::SKIP_DOTS);
foreach ($iterator as $item) {
/** @var SplFileInfo $item */
if ($item->isDir()) {
continue;
}

$extension = strtolower($item->getExtension());
if ($extension !== 'yaml' && $extension !== 'yml') {
continue;
}

$path = $item->getPathname();
$content = file_get_contents($path);
if ($content === false) {
throw new InvalidContentConfigException(
"Cannot read site data file: $path",
$path,
'Check that the file exists and is readable by the build process.',
);
}

$parsed = @yaml_parse($content);
if ($parsed === false) {
throw new InvalidContentConfigException(
"Invalid YAML in site data file: $path",
$path,
'Fix the YAML syntax in content/data/*.yaml, then run the build again.',
);
}
Comment on lines +51 to +58

$key = basename($item->getFilename(), '.' . $extension);
$data[$key] = $parsed;
}

ksort($data, SORT_STRING);

return $data;
}
}
27 changes: 27 additions & 0 deletions tests/Unit/Build/EntryRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,31 @@ public function testRendersWithCustomLayout(): void
assertStringContainsString('Wide content.', $html);
}

public function testTemplateCanReadSiteData(): void
{
mkdir($this->contentDir . '/templates', 0o755, true);
file_put_contents($this->contentDir . '/templates/data.php', <<<'PHP'
<?php

declare(strict_types=1);
?>
<h1><?= $h($data['company']['name']) ?></h1>
<a href="<?= $h($data['company']['links'][0]) ?>">About</a>
PHP);

$entryFile = $this->contentDir . '/blog/data.md';
file_put_contents($entryFile, "---\ntitle: Data Post\nlayout: data\n---\n\nBody.\n");

$entry = $this->createEntry(filePath: $entryFile, title: 'Data Post', layout: 'data');
$renderer = new EntryRenderer($this->createPipeline(), $this->createTemplateResolver($this->contentDir . '/templates'), contentDir: $this->contentDir);
$html = $renderer->render($this->createSiteConfig(theme: 'custom', data: [
'company' => ['name' => 'Acme', 'links' => ['/about/']],
]), $entry);

assertStringContainsString('<h1>Acme</h1>', $html);
assertStringContainsString('<a href="/about/">About</a>', $html);
}

public function testProvidesTranslationHelperToCustomEntryTemplates(): void
{
mkdir($this->contentDir . '/templates', 0o755, true);
Expand Down Expand Up @@ -589,6 +614,7 @@ private function createSiteConfig(
?string $reportIssueUrl = null,
bool $authorPages = false,
bool $minify = true,
array $data = [],
): SiteConfig
{
return new SiteConfig(
Expand All @@ -611,6 +637,7 @@ private function createSiteConfig(
reportIssueUrl: $reportIssueUrl,
authorPages: $authorPages,
minify: $minify,
data: $data,
);
}

Expand Down
16 changes: 16 additions & 0 deletions tests/Unit/Content/Parser/ContentParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ public function testParseCollections(): void
assertSame('Pages', $collections['page']->title);
}

public function testDataDirectoryIsNotParsedAsCollection(): void
{
$dir = sys_get_temp_dir() . '/yiipress-content-data-' . uniqid();
mkdir($dir . '/data', 0o755, true);
file_put_contents($dir . '/data/_collection.yaml', "title: Data\n");

try {
assertSame([], $this->parser->parseCollections($dir));
assertSame([], iterator_to_array($this->parser->parseAllEntries($dir), false));
} finally {
unlink($dir . '/data/_collection.yaml');
rmdir($dir . '/data');
rmdir($dir);
}
}

public function testParseRootCollection(): void
{
$dir = sys_get_temp_dir() . '/yiipress-root-collection-' . uniqid();
Expand Down
Loading
Loading