Keep the public site readable after import (nbsp, highlighter, wrapping code, compact pager) and finish the admin extras already in this tree: sticky levels, preview, page cache, and live llms.txt. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
3.3 KiB
PHP
121 lines
3.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Blog\Controllers;
|
|
|
|
use App\Models\Post;
|
|
use Illuminate\Support\Str;
|
|
use League\HTMLToMarkdown\HtmlConverter;
|
|
|
|
class LlmsController extends Controller
|
|
{
|
|
/**
|
|
* /llms.txt — live index for agents. Not a static file.
|
|
*/
|
|
public function index()
|
|
{
|
|
$siteName = (string) blog_setting('site_name', config('blog.name'));
|
|
$siteDescription = (string) blog_setting('site_description', config('blog.description'));
|
|
|
|
$posts = Post::query()
|
|
->published()
|
|
->latest('published_at')
|
|
->limit(200)
|
|
->get();
|
|
|
|
$lines = [
|
|
'# '.$siteName,
|
|
'',
|
|
'> '.$siteDescription,
|
|
'',
|
|
'## Feeds',
|
|
'',
|
|
'- [RSS]('.route('feed.rss').')',
|
|
'- [Sitemap]('.route('feed.sitemap').')',
|
|
'- [llms.txt]('.route('feed.llms').')',
|
|
'',
|
|
'## Articles',
|
|
'',
|
|
];
|
|
|
|
foreach ($posts as $post) {
|
|
if ($this->isRestricted($post)) {
|
|
continue;
|
|
}
|
|
|
|
$summary = Str::limit(trim(preg_replace('/\s+/', ' ', strip_tags($post->excerpt_or_fallback)) ?? ''), 120);
|
|
$markdownUrl = $this->markdownUrl($post);
|
|
$htmlUrl = $post->url;
|
|
$title = $post->title;
|
|
|
|
$lines[] = $markdownUrl
|
|
? '- ['.$title.']('.$markdownUrl.') — '.$summary.' ([html]('.$htmlUrl.'))'
|
|
: '- ['.$title.']('.$htmlUrl.') — '.$summary;
|
|
}
|
|
|
|
return response(implode("\n", $lines)."\n", 200, [
|
|
'Content-Type' => 'text/plain; charset=utf-8',
|
|
'Cache-Control' => 'no-store',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* /posts/{slug}.md — full markdown for a published public post.
|
|
*/
|
|
public function postMarkdown(string $slug)
|
|
{
|
|
$post = Post::query()
|
|
->where('slug', $slug)
|
|
->orWhere('id', (int) $slug)
|
|
->first();
|
|
|
|
abort_unless($post && $post->status === 'published' && ! $this->isRestricted($post), 404);
|
|
|
|
$body = $post->content_format === 'markdown'
|
|
? (string) $post->content
|
|
: $this->htmlToMarkdown((string) $post->content);
|
|
|
|
$out = '# '.$post->title."\n\n";
|
|
if ($post->excerpt) {
|
|
$out .= '> '.$post->excerpt."\n\n";
|
|
}
|
|
$out .= $body."\n";
|
|
|
|
return response($out, 200, [
|
|
'Content-Type' => 'text/markdown; charset=utf-8',
|
|
'Cache-Control' => 'no-store',
|
|
]);
|
|
}
|
|
|
|
private function isRestricted(Post $post): bool
|
|
{
|
|
if (filled($post->read_password)) {
|
|
return true;
|
|
}
|
|
|
|
return ! empty(($post->meta ?? [])['members_only']);
|
|
}
|
|
|
|
private function markdownUrl(Post $post): ?string
|
|
{
|
|
$slug = trim((string) $post->slug);
|
|
|
|
if ($slug === '') {
|
|
return route('geo.post.markdown', ['slug' => (string) $post->id]);
|
|
}
|
|
|
|
return route('geo.post.markdown', ['slug' => $slug]);
|
|
}
|
|
|
|
private function htmlToMarkdown(string $html): string
|
|
{
|
|
$converter = new HtmlConverter([
|
|
'strip_tags' => false,
|
|
'hard_break' => true,
|
|
]);
|
|
|
|
return $converter->convert($html);
|
|
}
|
|
}
|