update .gitignore, enhance README, implement Pexels API integration, and improve page generation logic

This commit is contained in:
2026-05-17 21:56:54 +02:00
parent ed7eeda24c
commit 135e1ac682
5 changed files with 411 additions and 91 deletions

View File

@@ -4,6 +4,137 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
function readLocalEnvValue(string $envFilePath, string $key): string
{
if (! is_file($envFilePath) || ! is_readable($envFilePath)) {
return '';
}
$lines = file($envFilePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
return '';
}
foreach ($lines as $line) {
$trimmed = trim($line);
if ($trimmed === '' || str_starts_with($trimmed, '#')) {
continue;
}
$parts = explode('=', $trimmed, 2);
if (count($parts) !== 2) {
continue;
}
if (trim($parts[0]) !== $key) {
continue;
}
return trim(trim($parts[1]), "\"'");
}
return '';
}
function extractHtmlTitle(string $html): string
{
if (preg_match('/<title>(.*?)<\/title>/is', $html, $matches) === 1) {
return trim(strip_tags($matches[1]));
}
return '';
}
/**
* Fetch one landscape-ish photo from Pexels for the given query.
*
* @return array{url:string,alt:string,photographer:string,photoUrl:string}|null
*/
function fetchPexelsPhoto(string $pexelsKey, string $query): ?array
{
if ($pexelsKey === '' || $query === '') {
return null;
}
$endpoint = 'https://api.pexels.com/v1/search?query=' . rawurlencode($query) . '&per_page=1&orientation=landscape&size=large';
$raw = false;
if (function_exists('curl_init')) {
$ch = curl_init($endpoint);
if ($ch !== false) {
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: ' . $pexelsKey,
'Accept: application/json',
],
CURLOPT_TIMEOUT => 12,
]);
$raw = curl_exec($ch);
curl_close($ch);
}
}
if ($raw === false) {
$context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => "Authorization: {$pexelsKey}\r\nAccept: application/json\r\n",
'timeout' => 12,
],
]);
$raw = @file_get_contents($endpoint, false, $context);
}
if ($raw === false) {
return null;
}
$json = json_decode($raw, true);
if (! is_array($json) || ! isset($json['photos'][0]) || ! is_array($json['photos'][0])) {
return null;
}
$photo = $json['photos'][0];
$src = $photo['src'] ?? [];
$url = (string) ($src['landscape'] ?? $src['large'] ?? $src['original'] ?? '');
if ($url === '') {
return null;
}
return [
'url' => $url,
'alt' => trim((string) ($photo['alt'] ?? 'Hero image')),
'photographer' => trim((string) ($photo['photographer'] ?? 'Pexels photographer')),
'photoUrl' => trim((string) ($photo['url'] ?? 'https://www.pexels.com/')),
];
}
/**
* Replace hero placeholders with Pexels image metadata.
*/
function injectHeroImagePlaceholders(string $html, ?array $photo): string
{
$fallbackSvg = rawurlencode('<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="627"><rect width="100%" height="100%" fill="#111827"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="#9CA3AF" font-family="Arial, sans-serif" font-size="36">Hero image unavailable</text></svg>');
$fallbackUrl = 'data:image/svg+xml;utf8,' . $fallbackSvg;
$imageUrl = $photo['url'] ?? $fallbackUrl;
$imageAlt = $photo['alt'] ?? 'Hero image';
$photographer = $photo['photographer'] ?? 'Pexels';
$photoUrl = $photo['photoUrl'] ?? 'https://www.pexels.com/';
$replacements = [
'__PEXELS_HERO_IMAGE__' => htmlspecialchars((string) $imageUrl, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
'__PEXELS_HERO_ALT__' => htmlspecialchars((string) $imageAlt, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
'__PEXELS_PHOTOGRAPHER__' => htmlspecialchars((string) $photographer, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
'__PEXELS_PHOTO_URL__' => htmlspecialchars((string) $photoUrl, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
];
return strtr($html, $replacements);
}
/**
* Render a JPG preview for a generated page using a local headless browser.
*/
@@ -102,6 +233,7 @@ header('Content-Type: text/html; charset=UTF-8');
$startedAt = microtime(true);
$pexelsKey = 'Clyqp9i9pewaxiiAdqVya0yZuuedjHtUtJdjRipNltp7wf0dY8K10h4h';
$apiKey = getenv('LLAMA_API_KEY') ?: '';
$model = getenv('LLAMA_MODEL') ?: 'local-model';
$maxTokens = (int) (getenv('LLAMA_MAX_TOKENS') ?: 4096);
@@ -156,7 +288,7 @@ You are a senior creative web designer and front-end engineer.
Task:
- On every run, first choose a unique page concept with a specific topic, visual theme, and style direction.
- Then generate one complete, production-ready HTML document from scratch.
- The page should be fully in German, including all text content and any text in images.
- The page should be fully in English, including all text content and any text in images.
Hard output rules:
- Return only raw HTML. No markdown. No code fences. No explanations.
@@ -166,6 +298,9 @@ Hard output rules:
- Inline any custom CSS and JavaScript in <style> and <script> tags.
- Make the page responsive and fully usable on mobile and desktop.
- Include accessible semantic structure and visible focus states.
- In the hero section, include one main image with: src="__PEXELS_HERO_IMAGE__" and alt="__PEXELS_HERO_ALT__".
- Add a small attribution near that image: "Photo by __PEXELS_PHOTOGRAPHER__ on Pexels" and link to __PEXELS_PHOTO_URL__.
- Do not use other stock-image providers or random external image URLs for the hero image.
Creative direction:
- Avoid generic templates.
@@ -223,6 +358,14 @@ try {
throw new RuntimeException('Model did not return a full HTML document.');
}
$imageQuery = $topicHint !== '' ? $topicHint : extractHtmlTitle($html);
if ($imageQuery === '') {
$imageQuery = $selectedCategory;
}
$pexelsPhoto = fetchPexelsPhoto($pexelsKey, $imageQuery);
$html = injectHeroImagePlaceholders($html, $pexelsPhoto);
$timingBlock = '<div style="position:fixed;right:12px;bottom:12px;z-index:2147483647;background:rgba(17,24,39,.92);color:#fff;padding:8px 10px;border-radius:10px;font:12px/1.3 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;box-shadow:0 8px 20px rgba(0,0,0,.28)">Generated in ' . $elapsedSeconds . 's</div>';
if (stripos($html, '</body>') !== false) {
$html = preg_replace('/<\/body>/i', $timingBlock . '</body>', $html, 1) ?? ($html . $timingBlock);