Skip to content

Commit 4e81a01

Browse files
authored
Merge pull request #273 from dotkernel/develop
Develop
2 parents b4b1486 + 9f9a7fd commit 4e81a01

11 files changed

Lines changed: 104 additions & 14 deletions

File tree

‎public/css/app.css‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎public/js/app.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/md-articles/best-practice/ai-native-coding-pods.md renamed to public/md-articles/headless-platform/ai-native-coding-pods-the-promise-risks-and-how-to-make-them-work.md

File renamed without changes.

public/md-articles/headless-platform/dotkernel-vs-lumen-slim-microservices.md renamed to public/md-articles/headless-platform/dotkernel-vs-lumen-vs-slim-which-is-better-for-a-lightweight-api-microservice.md

File renamed without changes.

‎src/App/assets/scss/components/_blog.scss‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,60 @@ $bp-tablet: 900px;
240240
font-size: inherit;
241241
}
242242
}
243+
244+
// Markdown tables render as bare <table> elements, so they are styled by tag.
245+
// The template wraps each one in .table-wrap so a wide table scrolls sideways
246+
// instead of overflowing the column.
247+
.table-wrap {
248+
margin: 0 0 24px;
249+
overflow-x: auto;
250+
border: 1px solid var(--border);
251+
border-radius: 8px;
252+
background: var(--surface);
253+
}
254+
255+
table {
256+
width: 100%;
257+
margin: 0;
258+
border-collapse: collapse;
259+
font-size: 14.5px;
260+
261+
th,
262+
td {
263+
padding: 12px 16px;
264+
text-align: left;
265+
vertical-align: top;
266+
border-bottom: 1px solid var(--border);
267+
color: var(--text);
268+
}
269+
270+
thead th {
271+
font-family: var(--mono);
272+
font-size: 12px;
273+
letter-spacing: 0.06em;
274+
text-transform: uppercase;
275+
white-space: nowrap;
276+
color: var(--text-dim);
277+
background: var(--surface-2);
278+
}
279+
280+
tbody tr:last-child td,
281+
tbody tr:last-child th {
282+
border-bottom: 0;
283+
}
284+
285+
tbody th,
286+
td:first-child {
287+
font-weight: 500;
288+
color: var(--text-muted);
289+
}
290+
291+
p {
292+
margin-bottom: 0;
293+
font-size: inherit;
294+
line-height: 1.6;
295+
}
296+
}
243297
}
244298

245299
// ---------- Obsolete post warning ----------

‎src/App/src/Fixture/articles_cleaned.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3103,7 +3103,7 @@
31033103
{
31043104
"post_title": "AI-Native Coding Pods: The Promise, Risks, and How to Make Them Work",
31053105
"post_date": "2026-09-09 12:00:00",
3106-
"post_status": "draft",
3106+
"post_status": "published",
31073107
"author": {
31083108
"display_name": "Florin Bidirean",
31093109
"github": "bidi47"

‎src/App/src/Service/FaqExtractor.php‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@
1414
/**
1515
* Pulls the trailing `## FAQ` section (a `**Q: ...**` / `A: ...` pair per question) out of a
1616
* Markdown article body, so it can be rendered as an accordion and fed into JSON-LD separately
17-
* from the rest of the body.
17+
* from the rest of the body. `question` and `answer` keep their Markdown (for rendering);
18+
* `questionText` and `answerText` are plain-text copies (for JSON-LD).
1819
*/
1920
final class FaqExtractor
2021
{
2122
/**
22-
* @return array{faq: list<array{question: string, answer: string}>, body: string}
23+
* @return array{
24+
* faq: list<array{question: string, answer: string, questionText: string, answerText: string}>,
25+
* body: string
26+
* }
2327
*/
2428
public static function extract(string $body): array
2529
{
@@ -35,14 +39,24 @@ public static function extract(string $body): array
3539
PREG_SET_ORDER
3640
);
3741
foreach ($matches as $match) {
42+
$question = trim($match[1]);
43+
$answer = trim(preg_replace('/\s+/', ' ', $match[2]) ?? '');
44+
3845
$faq[] = [
39-
'question' => trim($match[1]),
40-
'answer' => trim(preg_replace('/\s+/', ' ', $match[2]) ?? ''),
46+
'question' => $question,
47+
'answer' => $answer,
48+
'questionText' => self::toPlainText($question),
49+
'answerText' => self::toPlainText($answer),
4150
];
4251
}
4352

4453
$body = trim((string) preg_replace('/^## FAQ\s*$.*\z/ms', '', $body));
4554

4655
return ['faq' => $faq, 'body' => $body];
4756
}
57+
58+
private static function toPlainText(string $markdown): string
59+
{
60+
return preg_replace('/(`+)(.+?)\1/', '$2', $markdown) ?? $markdown;
61+
}
4862
}

‎src/App/templates/layout/blog-post.html.twig‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@id": "{{ app.url ~ path('page::blog-resource', {categorySlug: article.category.slug, slug: article.slug}) }}#faq",
4646
"mainEntity": [
4747
{% for item in faq %}
48-
{ "@type": "Question", "name": {{ item.question|json_encode|raw }}, "acceptedAnswer": { "@type": "Answer", "text": {{ item.answer|json_encode|raw }} } }{% if not loop.last %},{% endif %}
48+
{ "@type": "Question", "name": {{ item.questionText|json_encode|raw }}, "acceptedAnswer": { "@type": "Answer", "text": {{ item.answerText|json_encode|raw }} } }{% if not loop.last %},{% endif %}
4949
{% endfor %}
5050
]
5151
}{% endif %}

‎src/Blog/templates/page/markdown-article.html.twig‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
{% extends '@layout/blog-post.html.twig' %}
22

33
{% block body %}
4-
{{ content|markdown_to_html }}
4+
{{ content|markdown_to_html|replace({'<table>': '<div class="table-wrap"><table>', '</table>': '</table></div>'})|raw }}
55

66
{% if faq is not empty %}
77
<h2 id="user-content-faq">Frequently Asked Questions</h2>
88

99
<div class="faq-list">
1010
{% for item in faq %}
1111
<details class="acc"{% if loop.first %} open{% endif %}>
12-
<summary>{{ item.question }} <span class="chev">+</span></summary>
12+
<summary>{{ item.question|markdown_to_html|striptags('<code><strong><em><a>')|raw }} <span class="chev">+</span></summary>
1313
<div class="acc-body">
14-
<p class="mb-0">{{ item.answer }}</p>
14+
<p class="mb-0">{{ item.answer|markdown_to_html|striptags('<code><strong><em><a>')|raw }}</p>
1515
</div>
1616
</details>
1717
{% endfor %}

‎test/Unit/App/Service/FaqExtractorTest.php‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,35 @@ public function testExtractPullsTheQuestionAnswerPairsOutOfTheFaqSection(): void
2020
$this->assertSame('Some intro text.', $result['body']);
2121
$this->assertSame(
2222
[
23-
['question' => 'What is it?', 'answer' => "It's an example."],
24-
['question' => 'Why does it matter?', 'answer' => 'Because tests need coverage.'],
23+
[
24+
'question' => 'What is it?',
25+
'answer' => "It's an example.",
26+
'questionText' => 'What is it?',
27+
'answerText' => "It's an example.",
28+
],
29+
[
30+
'question' => 'Why does it matter?',
31+
'answer' => 'Because tests need coverage.',
32+
'questionText' => 'Why does it matter?',
33+
'answerText' => 'Because tests need coverage.',
34+
],
2535
],
2636
$result['faq']
2737
);
2838
}
2939

40+
public function testExtractKeepsMarkdownForRenderingAndStripsBackticksForPlainText(): void
41+
{
42+
$body = "## FAQ\n\n**Q: Is `dot-cache` needed?**\nA: Use `dot-cache` and ``dot-cli``.";
43+
44+
$result = FaqExtractor::extract($body);
45+
46+
$this->assertSame('Is `dot-cache` needed?', $result['faq'][0]['question']);
47+
$this->assertSame('Use `dot-cache` and ``dot-cli``.', $result['faq'][0]['answer']);
48+
$this->assertSame('Is dot-cache needed?', $result['faq'][0]['questionText']);
49+
$this->assertSame('Use dot-cache and dot-cli.', $result['faq'][0]['answerText']);
50+
}
51+
3052
public function testExtractReturnsAnEmptyFaqWhenThereIsNoFaqSection(): void
3153
{
3254
$body = "Some intro text.\n\nMore content.";

0 commit comments

Comments
 (0)