Skip to content

Commit 76ffbb4

Browse files
committed
How to add post and re-generate llms full
1 parent 99cb045 commit 76ffbb4

11 files changed

Lines changed: 253 additions & 1 deletion

File tree

bin/create-uploads-dir

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,4 @@ printf(
150150
$imagesCopied === 1 ? '' : 's',
151151
$imagesMissing,
152152
PHP_EOL
153-
);
153+
);

bin/generate-llms-full

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
declare(strict_types=1);
5+
6+
use Light\App\Service\LlmsFullGenerator;
7+
8+
chdir(__DIR__ . '/../');
9+
10+
require 'vendor/autoload.php';
11+
12+
$container = require 'config/container.php';
13+
$llmsFullGenerator = $container->get(LlmsFullGenerator::class);
14+
15+
$count = $llmsFullGenerator->write();
16+
17+
printf(
18+
"Done. %d article%s written to %s%s",
19+
$count,
20+
$count === 1 ? '' : 's',
21+
$llmsFullGenerator->getOutputFile(),
22+
PHP_EOL
23+
);

config/autoload/local.php.dist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ return [
5959
'feed' => [
6060
'path' => realpath(__DIR__ . '/../../public/feed.xml'),
6161
],
62+
'llms' => [
63+
'sourceDir' => realpath(__DIR__ . '/../../public/md-articles'),
64+
'outputFile' => realpath(__DIR__ . '/../../public/llms-full.txt'),
65+
],
6266
/**
6367
* Credentials for `bin/generate-packages`. Reading public repositories needs no token
6468
* scopes at all; without a token the generator still runs, at a lower rate limit.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
title: "Test article"
3+
description: "This is a test article used for fixture and testing purposes."
4+
author: "admin"
5+
date_published: "2026-08-06"
6+
canonical_url: "https://www.dotkernel.com/dotkernel/test-article/"
7+
category: "Dotkernel"
8+
language: "en"
9+
---
10+
11+
# Test article
12+
13+
## TL;DR
14+
15+
This is a test article added to the dotkernel category for testing purposes.
16+
17+
## Overview
18+
19+
This is a test article used for fixture and testing purposes.
20+
21+
## FAQ
22+
23+
**Q: What is this article for?**
24+
A: This is a test article added to the Dotkernel category for fixture and testing purposes.

public/uploads/test-article.png

952 KB
Loading

src/App/src/ConfigProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Light\App\Factory\GetPackagesViewHandlerFactory;
2020
use Light\App\Factory\GetSitemapViewHandlerFactory;
2121
use Light\App\Factory\GitHubClientFactory;
22+
use Light\App\Factory\LlmsFullGeneratorFactory;
2223
use Light\App\Factory\PackageGeneratorFactory;
2324
use Light\App\Factory\SitemapGeneratorFactory;
2425
use Light\App\Handler\GetFeedViewHandler;
@@ -30,6 +31,7 @@
3031
use Light\App\Service\FeedGenerator;
3132
use Light\App\Service\GitHubClient;
3233
use Light\App\Service\GitHubClientInterface;
34+
use Light\App\Service\LlmsFullGenerator;
3335
use Light\App\Service\PackageGenerator;
3436
use Light\App\Service\SitemapGenerator;
3537
use Mezzio\Application;
@@ -138,6 +140,7 @@ public function getDependencies(): array
138140
SitemapGenerator::class => SitemapGeneratorFactory::class,
139141
GitHubClient::class => GitHubClientFactory::class,
140142
PackageGenerator::class => PackageGeneratorFactory::class,
143+
LlmsFullGenerator::class => LlmsFullGeneratorFactory::class,
141144
],
142145
'aliases' => [
143146
EntityManager::class => 'doctrine.entity_manager.orm_default',
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Light\App\Factory;
6+
7+
use Light\App\Service\LlmsFullGenerator;
8+
use Psr\Container\ContainerInterface;
9+
10+
class LlmsFullGeneratorFactory
11+
{
12+
public function __invoke(ContainerInterface $container): LlmsFullGenerator
13+
{
14+
$config = $container->get('config');
15+
16+
return new LlmsFullGenerator(
17+
$config['llms']['sourceDir'],
18+
$config['llms']['outputFile'],
19+
$config['application']['baseUrl'] ?? '',
20+
);
21+
}
22+
}

src/App/src/Fixture/articles_cleaned.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,19 @@
835835
"opengraph_img": "\/opengraph\/article\/twitter-card-symfony-mailer.png",
836836
"excerpt": "What prompted the change According to the discussion from the LaminasTechnical steering Committee of 2023-12-04, it was decided that the laminas\/laminas-mail package would be abandoned. On the one hand, there is nobody to maintain the package and on the other, there are several alternatives available in the ecosystem: ddeboer\/imap for interacting with IMAP zbateson\/mail-mime-parser for parsing MIME messages symfony\/mailer for sending mail How Dotkernel handles the issue The Dotkernel team has also opted to replace the laminas\/laminas-mail package in the dotkernel\/dot-mail package.",
837837
"tl_dr": "The Laminas Technical Steering Committee decided on 2023-12-04 to abandon laminas\/laminas-mail.\nDotkernel responded by replacing it with symfony\/mailer inside the dotkernel\/dot-mail package (version 5), aiming for minimal impact on existing projects — calls to send mail stay the same, though mime and imap related functionality is removed."
838+
},
839+
{
840+
"post_title": "Test article",
841+
"post_date": "2026-08-06 00:00:00",
842+
"post_status": "publish",
843+
"author": {
844+
"display_name": "stefan",
845+
"github": "OStefan2001"
846+
},
847+
"isObsolete": false,
848+
"opengraph_img": "\/opengraph\/article\/twitter-card-symfony-mailer.png",
849+
"excerpt": "This is a test article used for fixture and testing purposes.",
850+
"tl_dr": "This is a test article added to the dotkernel category for testing purposes."
838851
}
839852
]
840853
},
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Light\App\Service;
6+
7+
use RuntimeException;
8+
9+
use function array_map;
10+
use function array_merge;
11+
use function count;
12+
use function file_get_contents;
13+
use function file_put_contents;
14+
use function glob;
15+
use function implode;
16+
use function is_file;
17+
use function ltrim;
18+
use function sort;
19+
use function sprintf;
20+
use function str_replace;
21+
use function trim;
22+
23+
/**
24+
* Concatenates every article under the markdown source directory into a single
25+
* plain-text file for LLM consumption, in the same format as llms.txt/llms-full.txt
26+
* conventions (index.md first, then every other article sorted by relative path).
27+
*/
28+
readonly class LlmsFullGenerator
29+
{
30+
private const SEPARATOR = '<!-- ============================================================ -->';
31+
32+
public function __construct(
33+
private string $sourceDir,
34+
private string $outputFile,
35+
private string $baseUrl,
36+
) {
37+
}
38+
39+
public function getOutputFile(): string
40+
{
41+
return $this->outputFile;
42+
}
43+
44+
public function write(): int
45+
{
46+
$relativePaths = $this->collectRelativePaths();
47+
48+
$sections = array_map(
49+
fn (string $relativePath): string => $this->buildSection($relativePath),
50+
$relativePaths
51+
);
52+
53+
$written = implode("\n\n\n", $sections) . "\n\n";
54+
55+
if (file_put_contents($this->outputFile, $written) === false) {
56+
throw new RuntimeException('Unable to write llms-full.txt.');
57+
}
58+
59+
return count($relativePaths);
60+
}
61+
62+
/**
63+
* @return list<string>
64+
*/
65+
private function collectRelativePaths(): array
66+
{
67+
$categoryFiles = glob($this->sourceDir . '/*/*.md') ?: [];
68+
$categoryFiles = array_map(
69+
fn (string $path): string => ltrim(str_replace($this->sourceDir, '', $path), '/'),
70+
$categoryFiles
71+
);
72+
sort($categoryFiles);
73+
74+
$paths = is_file($this->sourceDir . '/index.md') ? ['index.md'] : [];
75+
76+
return array_merge($paths, $categoryFiles);
77+
}
78+
79+
private function buildSection(string $relativePath): string
80+
{
81+
$contents = file_get_contents($this->sourceDir . '/' . $relativePath);
82+
if ($contents === false) {
83+
throw new RuntimeException("Unable to read file: {$relativePath}");
84+
}
85+
86+
$contents = str_replace('{{base_url}}', $this->baseUrl, $contents);
87+
88+
return sprintf(
89+
"%s\n<!-- %s -->\n%s\n\n%s",
90+
self::SEPARATOR,
91+
$relativePath,
92+
self::SEPARATOR,
93+
trim($contents)
94+
);
95+
}
96+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script type="application/ld+json">
2+
{
3+
"@context": "https://schema.org",
4+
"@graph": [
5+
{
6+
"@type": "TechArticle",
7+
"@id": "{{ absolute_url(path('page::blog-resource', {categorySlug: article.category.slug, slug: article.slug})) }}#article",
8+
"headline": {{ article.title|json_encode|raw }},
9+
"description": {{ article.excerpt|json_encode|raw }},
10+
"url": "{{ absolute_url(path('page::blog-resource', {categorySlug: article.category.slug, slug: article.slug})) }}",
11+
"mainEntityOfPage": "{{ absolute_url(path('page::blog-resource', {categorySlug: article.category.slug, slug: article.slug})) }}",
12+
"datePublished": "{{ article.postDate|date('c') }}",
13+
"dateModified": "{{ article.postDate|date('c') }}",
14+
"image": "{{ app.meta.image }}",
15+
"articleSection": {{ article.category.name|json_encode|raw }},
16+
"author": {
17+
"@type": "Person",
18+
"name": {{ article.author.name|json_encode|raw }},
19+
"url": "{{ absolute_url(path('page::author-resource', {slug: article.author.slug})) }}"
20+
},
21+
"publisher": {
22+
"@type": "Organization",
23+
"name": "Dotkernel",
24+
"url": "{{ absolute_url('/') }}",
25+
"logo": {
26+
"@type": "ImageObject",
27+
"url": "{{ absolute_url(asset('images/app/dotkernel-logo.png')) }}"
28+
}
29+
}
30+
},
31+
{
32+
"@type": "BreadcrumbList",
33+
"@id": "{{ absolute_url(path('page::blog-resource', {categorySlug: article.category.slug, slug: article.slug})) }}#breadcrumb",
34+
"itemListElement": [
35+
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "{{ absolute_url('/') }}" },
36+
{ "@type": "ListItem", "position": 2, "name": {{ article.category.name|json_encode|raw }}, "item": "{{ absolute_url(path('page::category-resource', {slug: article.category.slug})) }}" },
37+
{ "@type": "ListItem", "position": 3, "name": {{ article.title|json_encode|raw }} }
38+
]
39+
},
40+
{
41+
"@type": "FAQPage",
42+
"@id": "{{ absolute_url(path('page::blog-resource', {categorySlug: article.category.slug, slug: article.slug})) }}#faq",
43+
"mainEntity": [
44+
{ "@type": "Question", "name": "What is this article for?", "acceptedAnswer": { "@type": "Answer", "text": "This is a test article added to the Dotkernel category for fixture and testing purposes." } }
45+
]
46+
}
47+
]
48+
}
49+
</script>

0 commit comments

Comments
 (0)