diff --git a/config/autoload/headers.global.php b/config/autoload/headers.global.php new file mode 100644 index 00000000..e56a1b2b --- /dev/null +++ b/config/autoload/headers.global.php @@ -0,0 +1,73 @@ +; rel="llms-txt", ; rel="llms-full-txt"'; +$serviceDocLink = static fn (string $url): string => sprintf('<%s>; rel="service-doc", ', $url) . $llmsTxtLink; + +return [ + 'dot_response_headers' => [ + '*' => [ + 'X-Powered-By' => [ + 'value' => 'Dotkernel', + 'overwrite' => true, + ], + 'X-Llms-Txt' => [ + 'value' => '/llms.txt', + 'overwrite' => true, + ], + 'Link' => [ + 'value' => $llmsTxtLink, + 'overwrite' => true, + ], + ], + 'app::index' => [ + 'Link' => [ + 'value' => $serviceDocLink('https://docs.dotkernel.org/'), + 'overwrite' => true, + ], + ], + 'page::api' => [ + 'Link' => [ + 'value' => $serviceDocLink('https://docs.dotkernel.org/api-documentation/'), + 'overwrite' => true, + ], + ], + 'page::admin' => [ + 'Link' => [ + 'value' => $serviceDocLink('https://docs.dotkernel.org/admin-documentation/'), + 'overwrite' => true, + ], + ], + 'page::queue' => [ + 'Link' => [ + 'value' => $serviceDocLink('https://docs.dotkernel.org/queue-documentation/'), + 'overwrite' => true, + ], + ], + 'page::light' => [ + 'Link' => [ + 'value' => $serviceDocLink('https://docs.dotkernel.org/light-documentation/'), + 'overwrite' => true, + ], + ], + 'page::frontend' => [ + 'Link' => [ + 'value' => $serviceDocLink('https://docs.dotkernel.org/frontend/'), + 'overwrite' => true, + ], + ], + 'page::wsl2' => [ + 'Link' => [ + 'value' => $serviceDocLink('https://docs.dotkernel.org/development/v2/terminal/'), + 'overwrite' => true, + ], + ], + 'page::architecture' => [ + 'Link' => [ + 'value' => $serviceDocLink('https://docs.dotkernel.org/api-documentation/'), + 'overwrite' => true, + ], + ], + ], +]; diff --git a/config/autoload/local.php.dist b/config/autoload/local.php.dist index 57fc88e0..3cd2b2e1 100644 --- a/config/autoload/local.php.dist +++ b/config/autoload/local.php.dist @@ -47,35 +47,19 @@ $app = [ ]; return [ - 'application' => $app, - 'databases' => $databases, - 'dot_response_headers' => [ - '*' => [ - 'X-Powered-By' => [ - 'value' => $app['meta']['siteName'], - 'overwrite' => true, - ], - 'X-Llms-Txt' => [ - 'value' => '/llms.txt', - 'overwrite' => true, - ], - 'Link' => [ - 'value' => '; rel="llms-txt", ; rel="llms-full-txt"', - 'overwrite' => true, - ], - ], - ], - 'doctrine' => [ + 'application' => $app, + 'databases' => $databases, + 'doctrine' => [ 'connection' => [ 'orm_default' => [ 'params' => $databases['default'], ], ], ], - 'feed' => [ + 'feed' => [ 'path' => realpath(__DIR__ . '/../../public/feed.xml'), ], - 'llms' => [ + 'llms' => [ 'sourceDir' => realpath(__DIR__ . '/../../public/md-articles'), 'outputFile' => realpath(__DIR__ . '/../../public/llms-full.txt'), // Markdown versions of the static pages, appended after the articles. Optional. diff --git a/config/pipeline.php b/config/pipeline.php index ba6c27c9..6640698a 100644 --- a/config/pipeline.php +++ b/config/pipeline.php @@ -41,7 +41,6 @@ // Register the routing middleware in the middleware pipeline. // This middleware registers the Mezzio\Router\RouteResult request attribute. $app->pipe(RouteMiddleware::class); - $app->pipe(ResponseHeaderMiddleware::class); // The following handle routing failures for common conditions: // - HEAD request but no routes answer that method @@ -52,6 +51,7 @@ $app->pipe(ImplicitHeadMiddleware::class); $app->pipe(ImplicitOptionsMiddleware::class); $app->pipe(MethodNotAllowedMiddleware::class); + $app->pipe(ResponseHeaderMiddleware::class); // Seed the UrlHelper with the routing results: $app->pipe(UrlHelperMiddleware::class); diff --git a/public/md-articles/dotkernel-api/what-production-ready-means-for-dotkernel-api.md b/public/md-articles/dotkernel-api/what-production-ready-means-for-dotkernel-api.md new file mode 100644 index 00000000..46388052 --- /dev/null +++ b/public/md-articles/dotkernel-api/what-production-ready-means-for-dotkernel-api.md @@ -0,0 +1,103 @@ +--- +title: "What \"Production Ready\" Means for Dotkernel API" +description: "What Dotkernel API's production-ready claim actually covers, what still sits on you (rate limiting, gateway, federated identity, error tracking, health checks, caching), and where to read the full checklist before going live." +author: "arhimede" +date_published: "2026-09-09" +canonical_url: "https://www.dotkernel.com/dotkernel-api/what-production-ready-means-for-dotkernel-api/" +category: "Dotkernel API" +language: "en" +--- + +# What "Production Ready" Means for Dotkernel API + +## TL;DR + +Dotkernel API ships a complete, tested application layer — pipeline, OAuth2 auth, RBAC, validation, error responses, OpenAPI. +It does not ship the infrastructure layer around it: rate limiting, an API gateway, federated identity, error tracking, health checks, or caching/background jobs. +Those are deliberate boundaries, not gaps, and the linked documentation page lists each one with what to configure before going live. + +Dotkernel API describes itself as production ready, and it is — but the phrase covers less ground than most people assume, and the difference is worth naming. + +## What Ships in the Box + +A REST API needs a lot of machinery before it can serve a single useful request. +Dotkernel API brings all of it already assembled: a readable middleware pipeline, OAuth2 authentication, role-based access control, request validation, standardised error responses and a generated OpenAPI specification. +None of that is scaffolding you finish yourself. +It is written, tested and documented, and it is the reason you can go from an empty project to a working endpoint in an afternoon. + +That is what production ready means here. +The application is complete. + +## What You Still Own + +What it does not mean is that everything a public API needs on the day it goes live is included. +Around the application sits a second layer, and that layer is yours: + +- **Traffic control.** Nothing limits how fast a client can call your endpoints. + Your login endpoint will answer a brute-force attempt as patiently as it answers a real user. +- **An API gateway.** Certificates, per-customer quotas, API keys, edge caching and a web application firewall all live in front of the application, not inside it. +- **Federated identity.** Dotkernel API issues its own tokens. + If your organisation signs in through Keycloak, Auth0, Microsoft Entra ID or Okta, connecting the two is work you do. +- **Somewhere for errors to go.** Errors are written to a file on the server. + They are not sent to Sentry or any other tracking service, which means that by default, nobody is told when something breaks. +- **Health checks and monitoring.** There is no endpoint a load balancer can ask "are you actually working?", and no metrics coming out of the application at all. +- **Caching and background work.** Responses are not cached, and email is sent while the user waits. + +None of these are bugs. +Most of them are decisions. + +## Why the Line Is Drawn There + +Rate limiting is the clearest example. +It genuinely belongs in your infrastructure, where it can protect every node at once, rather than inside application code that only sees its own traffic. +The same argument applies to certificates, secrets and log shipping. +A framework that shipped opinionated versions of all of these would be a framework you spend your first week fighting. + +The real problem was never the missing pieces. +It was that nobody wrote down which pieces were missing. +A team can reasonably read "production ready", deploy on Friday, and only discover on Monday that the login endpoint has no rate limit — not because they were careless, but because nothing told them that half was theirs. + +## Where the Line Is Moving + +Some of it is moving inward. +There are open proposals to add a health check endpoint, to investigate a rate limiting middleware for demonstration purposes, and to extend caching. +The boundary is not fixed, and it is being discussed in the open. + +## Start Here + +We have added a page to the documentation that names every gap, says whether it belongs in your application or in the platform in front of it, and gives you the concrete thing to configure for each one. +It closes with a short ordered checklist of what to do before you go live. + +Read it before your first deployment, not after: + +**[Production Readiness — Dotkernel API documentation](https://docs.dotkernel.org/api-documentation/v7/reference/production-readiness/)** + +## FAQ + +**Is Dotkernel API safe to run in production?** + +Yes, with the layer around it in place. +The application itself is complete and tested; what needs attention is the infrastructure it runs on and a handful of settings you should review before launch. + +**Is this different from other PHP frameworks?** + +Not really. +Very few frameworks ship rate limiting, monitoring or identity federation, and the ones that do usually ship a version you end up replacing. +What is different here is that we have written down where the line falls instead of leaving you to find it. + +**How long does the missing half take?** + +Less time than you would expect, if you know about it in advance. +Most of the list is configuration in your web server or gateway rather than code, and the documentation page gives you a priority order so you can start with what matters most. + +**Will these become part of Dotkernel API?** + +Some of them. +Health checks, caching and a demonstration rate limiter are all under discussion. +Others, such as certificates and secret storage, will stay where they belong — in your infrastructure. + +## Resources + +- [Production Readiness](https://docs.dotkernel.org/api-documentation/v7/reference/production-readiness/) — the full documentation page +- [Basic Security](https://docs.dotkernel.org/api-documentation/v7/security/basic-security/) — application-level hardening before launch +- [Dotkernel API on GitHub](https://github.com/dotkernel/api) — issues and open proposals diff --git a/public/md-articles/how-to/how-to-use-twig-markdown-to-generate-html-pages-at-runtime.md b/public/md-articles/how-to/how-to-use-twig-markdown-to-generate-html-pages-at-runtime.md new file mode 100644 index 00000000..7f59efa0 --- /dev/null +++ b/public/md-articles/how-to/how-to-use-twig-markdown-to-generate-html-pages-at-runtime.md @@ -0,0 +1,182 @@ +--- +title: "How to Use Twig Markdown to Generate HTML Pages at Runtime" +description: "How to wire twig/markdown-extra with league/commonmark so a Twig application converts Markdown files to HTML on every request, instead of pre-compiling pages at build time." +author: "stefan" +date_published: "2026-09-09" +canonical_url: "https://www.dotkernel.com/how-to/how-to-use-twig-markdown-to-generate-html-pages-at-runtime/" +category: "How to's" +language: "en" +--- + +# How to Use Twig Markdown to Generate HTML Pages at Runtime + +## TL;DR + +`twig/markdown-extra` adds a `markdown_to_html` filter to Twig but ships no Markdown parser itself — you provide one through a `RuntimeLoaderInterface` factory. +Wire `MarkdownExtension` and a `MarkdownRuntime` (backed here by `league/commonmark`'s `GithubFlavoredMarkdownConverter`) into your container, register the extension with Twig, then call `{{ someMarkdownString|markdown_to_html }}` in any template. +Because the conversion runs inside the request/response cycle rather than at build time, the source `.md` file can be read from disk, edited, or swapped right up until the moment a request asks for it — no build step, no cache to invalidate by hand. + +## Why Runtime Instead of Build Time + +A static-site generator turns Markdown into HTML once, ahead of time, and serves the resulting files. +That works well when content changes rarely and a build pipeline is already part of the deploy. +Rendering at runtime is a better fit when: + +- Content lives in the same repository as the code and should go live the moment the file is merged, with no separate "build the site" step. +- The same Markdown source needs to serve more than one output — an HTML page, a raw `text/markdown` response for a bot or an LLM, an RSS/Atom feed excerpt — without maintaining a pre-rendered copy for each. +- A handler needs to inspect or transform the Markdown first (strip a section, extract structured data from it) before deciding what HTML to produce. + +The tradeoff is that every request pays the parsing cost, so pair it with an opcode cache and, if traffic warrants it, an HTTP or application cache layer — the parsing itself is not the expensive part until a page is read thousands of times a minute. + +## Step 1: Install the Packages + +Two packages are required: the Twig extension that exposes the filter, and a Markdown engine to actually do the conversion. + +```bash +composer require twig/markdown-extra league/commonmark +``` + +`twig/markdown-extra` is deliberately engine-agnostic — it defines the `markdown_to_html` filter and a `MarkdownInterface` contract, but leaves the choice of parser (CommonMark, Parsedown, michelf/php-markdown, or a custom one) to the application. + +## Step 2: Register the Extension and a Runtime Loader + +Twig extensions that need constructor arguments, or that wrap a heavier dependency, are usually built through the DI container rather than instantiated inline. Two pieces go into the container: the extension itself, and a `RuntimeLoaderInterface` that lazily builds the `MarkdownRuntime` the extension calls at render time. + +```php +// MarkdownExtensionFactory.php +use Psr\Container\ContainerInterface; +use Twig\Extra\Markdown\MarkdownExtension; + +class MarkdownExtensionFactory +{ + public function __invoke(ContainerInterface $container): MarkdownExtension + { + return new MarkdownExtension(); + } +} +``` + +```php +// MarkdownRuntimeLoaderFactory.php +use League\CommonMark\GithubFlavoredMarkdownConverter; +use Psr\Container\ContainerInterface; +use Twig\Extra\Markdown\LeagueMarkdown; +use Twig\Extra\Markdown\MarkdownRuntime; +use Twig\RuntimeLoader\FactoryRuntimeLoader; +use Twig\RuntimeLoader\RuntimeLoaderInterface; + +class MarkdownRuntimeLoaderFactory +{ + public function __invoke(ContainerInterface $container): RuntimeLoaderInterface + { + return new FactoryRuntimeLoader([ + MarkdownRuntime::class => static fn (): MarkdownRuntime => new MarkdownRuntime( + new LeagueMarkdown(new GithubFlavoredMarkdownConverter()) + ), + ]); + } +} +``` + +`LeagueMarkdown` is the small adapter `twig/markdown-extra` ships for `league/commonmark`; swapping engines later only means writing a different adapter (or using one of the built-in ones) inside this one factory — nothing in the templates has to change. + +Then register both in the Twig configuration, so the extension is loaded and the runtime loader is available to resolve it: + +```php +'twig' => [ + 'extensions' => [ + MarkdownExtension::class, + ], + 'runtime_loaders' => [ + RuntimeLoaderInterface::class, + ], +], +``` + +And map both classes to their factories in the container configuration: + +```php +'factories' => [ + MarkdownExtension::class => MarkdownExtensionFactory::class, + RuntimeLoaderInterface::class => MarkdownRuntimeLoaderFactory::class, +], +``` + +## Step 3: Convert Markdown to HTML in a Template + +With both pieces registered, any string of Markdown passed into a template can be converted with the filter: + +```twig +{{ content|markdown_to_html }} +``` + +`content` here is just a plain PHP string — commonly the body of a `.md` file read with `file_get_contents()` and handed to the template as a render variable. The filter runs CommonMark against it and outputs the resulting HTML, unescaped, directly into the page. + +## Step 4: Resolve the Source File Safely + +If the Markdown file path is built from anything in the request — a slug, a category, a page name in the URL — resolve it with `realpath()` and verify the result still lives inside the intended base directory before reading it. Otherwise a crafted slug like `../../../../etc/passwd` could walk outside the content directory: + +```php +public function resolveMarkdownFilePath(string $categorySlug, string $slug): ?string +{ + $base = realpath($this->articlesPath); + if ($base === false) { + return null; + } + + $realPath = realpath(rtrim($base, '/') . '/' . $categorySlug . '/' . $slug . '.md'); + if ($realPath === false || ! is_file($realPath)) { + return null; + } + + if (! str_starts_with($realPath, rtrim($base, '/') . '/')) { + return null; + } + + return $realPath; +} +``` + +`realpath()` returns `false` for a path that doesn't exist and also collapses any `..` segments, so the prefix check afterwards is what actually rejects a resolved path that escaped the articles directory — checking the raw, unresolved string wouldn't be enough on its own. + +## Putting It Together in a Handler + +A typical request handler ties the four steps into one flow: resolve the file, read it, optionally pull out metadata (front matter, an FAQ section, anything else the page needs), and hand the remaining body to the template that applies the filter. + +```php +$markdownFile = $this->blogService->resolveMarkdownFilePath($categorySlug, $slug); +if ($markdownFile === null) { + return $this->notFound(); +} + +$body = file_get_contents($markdownFile); + +return new HtmlResponse( + $this->template->render('page::article', ['content' => $body]) +); +``` + +Nothing here writes an HTML file to disk. The `.md` file is the only artifact that exists at rest; the HTML is produced fresh on every request and thrown away as soon as the response is sent. + +## Resources + +- [twig/markdown-extra documentation](https://twig.symfony.com/doc/3.x/filters/markdown_to_html.html) — the official filter reference +- [league/commonmark documentation](https://commonmark.thephpleague.com/) — the CommonMark/GFM converter used in the examples above +- [CommonMark specification](https://spec.commonmark.org/) — the Markdown dialect both packages implement + +## FAQ + +**Q: Does twig/markdown-extra include its own Markdown parser?** +A: No. It only defines the `markdown_to_html` filter and a `MarkdownInterface` contract; you must register a `RuntimeLoaderInterface` factory that builds a `MarkdownRuntime` backed by an actual engine, such as league/commonmark, Parsedown, or michelf/php-markdown. + +**Q: Why render Markdown at runtime instead of pre-building HTML files?** +A: Runtime rendering lets content go live the moment a file is merged, with no separate build step, and lets the same source serve more than one output (HTML, raw text/markdown, feed excerpts) from a single file. + +**Q: Is converting Markdown to HTML on every request expensive?** +A: Parsing itself is fast; the practical cost only shows up at high traffic. An opcode cache plus, if needed, an HTTP or application-level cache in front of the rendered response is enough to handle most load without pre-building anything. + +**Q: How do you prevent a URL from reading a file outside the content directory?** +A: Resolve the constructed path with `realpath()`, which returns `false` for paths that don't exist and collapses any `..` segments, then verify the resolved path still starts with the base content directory before reading the file. + +**Q: Can the Markdown engine be swapped later without touching templates?** +A: Yes. The engine is only referenced inside the `RuntimeLoaderInterface` factory; templates only ever call the `markdown_to_html` filter, so replacing `LeagueMarkdown` with a different adapter is a one-file change. diff --git a/public/opengraph/article/twitter-card-dotkernel-api-production-ready.png b/public/opengraph/article/twitter-card-dotkernel-api-production-ready.png new file mode 100644 index 00000000..b397fe80 Binary files /dev/null and b/public/opengraph/article/twitter-card-dotkernel-api-production-ready.png differ diff --git a/public/opengraph/article/twitter-card-markdown-to-html.png b/public/opengraph/article/twitter-card-markdown-to-html.png new file mode 100644 index 00000000..b9f21d06 Binary files /dev/null and b/public/opengraph/article/twitter-card-markdown-to-html.png differ diff --git a/src/App/assets/opengraph/article/twitter-card-dotkernel-api-production-ready.png b/src/App/assets/opengraph/article/twitter-card-dotkernel-api-production-ready.png new file mode 100644 index 00000000..b397fe80 Binary files /dev/null and b/src/App/assets/opengraph/article/twitter-card-dotkernel-api-production-ready.png differ diff --git a/src/App/assets/opengraph/article/twitter-card-markdown-to-html.png b/src/App/assets/opengraph/article/twitter-card-markdown-to-html.png new file mode 100644 index 00000000..b9f21d06 Binary files /dev/null and b/src/App/assets/opengraph/article/twitter-card-markdown-to-html.png differ diff --git a/src/App/src/Fixture/articles_cleaned.json b/src/App/src/Fixture/articles_cleaned.json index b4d4e336..ba73b29b 100644 --- a/src/App/src/Fixture/articles_cleaned.json +++ b/src/App/src/Fixture/articles_cleaned.json @@ -2010,6 +2010,21 @@ "excerpt": "With the recent release of AlmaLinux OS 10, we have created a new recipe for our WSL development environment. Compared to its predecessor, AlmaLinux 10 provides performance enhancements, security updates and improved hardware support.", "tl_dr": "With the release of AlmaLinux OS 10, Dotkernel created a new WSL2 development environment recipe offering performance, security, and hardware improvements over AlmaLinux 9.\nThe recipe sets up WSL2, AlmaLinux 10, PHP, Apache, MariaDB, Git, Composer, Node.js, and PhpMyAdmin.\nIt also covers the OS/hardware requirements, installing the distro, and running PHP projects directly or via virtual hosts.", "tags": [] + }, + { + "post_title": "How to Use Twig Markdown to Generate HTML Pages at Runtime", + "post_date": "2026-09-09 00:00:00", + "post_status": "published", + "author": { + "display_name": "stefan", + "github": "OStefan2001" + }, + "isObsolete": false, + "isTwig": false, + "opengraph_img": "/opengraph/article/twitter-card-markdown-to-html.png", + "excerpt": "How to wire twig/markdown-extra with league/commonmark so a Twig application converts Markdown files to HTML on every request, instead of pre-compiling pages at build time.", + "tl_dr": "twig/markdown-extra adds a markdown_to_html filter to Twig but ships no Markdown parser itself — you provide one through a RuntimeLoaderInterface factory.\nWire MarkdownExtension and a MarkdownRuntime (backed by league/commonmark's GithubFlavoredMarkdownConverter) into your container, register the extension with Twig, then call {{ someMarkdownString|markdown_to_html }} in any template.\nBecause the conversion runs inside the request/response cycle rather than at build time, the source .md file can be read from disk, edited, or swapped right up until the moment a request asks for it — no build step, no cache to invalidate by hand.", + "tags": [] } ] }, @@ -2783,6 +2798,21 @@ "excerpt": "Why We Switched to the Offline-Focused Bruno Every API developer knows that to build an API properly you need a reliable client for testing and interacting with the API. Ideally this tool should be free, it should store endpoint collections and share them easily with your team, and it should be fast and secure.", "tl_dr": "The team has used Postman for years but is considering switching to Bruno, a lightweight, offline-first alternative, reflecting a broader PHP community trend toward local-first, Git-native developer tools.\nBruno wins on offline access, version control via Git, performance, and (arguably) security, while Postman still offers a broader feature set for larger, budget-having teams.", "tags": [] + }, + { + "post_title": "What \"Production Ready\" Means for Dotkernel API", + "post_date": "2026-09-09 00:00:00", + "post_status": "published", + "author": { + "display_name": "arhimede", + "github": "arhimede" + }, + "isObsolete": false, + "isTwig": false, + "opengraph_img": "/opengraph/article/twitter-card-dotkernel-api-production-ready.png", + "excerpt": "Dotkernel API describes itself as production ready. Here is exactly what that covers — and the infrastructure layer (rate limiting, gateway, federated identity, error tracking, health checks, caching) that stays yours to add before launch.", + "tl_dr": "Dotkernel API ships a complete, tested application layer — pipeline, OAuth2 auth, RBAC, validation, error responses, OpenAPI. It does not ship the infrastructure layer around it: rate limiting, an API gateway, federated identity, error tracking, health checks, or caching/background jobs. Those are deliberate boundaries, not gaps, and the linked documentation page lists each one with what to configure before going live.", + "tags": [] } ] },