feat: restore sablog browse modes and clean imported HTML
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>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user