Skip to content

Commit 0490149

Browse files
authored
docs: Add social meta tags cookbook recipe (#95)
* docs: Add social meta tags cookbook recipe Add recipe for extracting Open Graph and Twitter Card metadata from Djot documents, including: - Basic extraction (title, description, image) - HTML meta tag generation - Custom extraction rules via div attributes - Fallback values - Framework integration example * fix: Address review feedback on social meta cookbook - Add missing Document import - Add SoftBreak/HardBreak handling in getTextContent() - Fix Image::getDestination() to Image::getSource() - Escape $url and $siteName in generateMetaTags() - Add Div import and fix misleading comment * fix: Address additional review feedback - Rename custom extractor to extractSocialMetaWithOverrides() to avoid function redeclaration conflict - Call basic extractor first, then override with div attributes - Use explicit null checks instead of truthy checks for consistency - Use null coalescing for array access in generateMetaTags() - Add note that framework example is pseudocode - Remove undefined Response return type
1 parent 0873b07 commit 0490149

1 file changed

Lines changed: 250 additions & 0 deletions

File tree

docs/cookbook/index.md

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Common recipes and customizations for djot-php.
2626
- [Alternative Output Formats](#alternative-output-formats)
2727
- [Soft Break Modes](#soft-break-modes)
2828
- [Significant Newlines Mode](#significant-newlines-mode)
29+
- [Social Meta Tags](#social-meta-tags)
2930

3031
## External Links
3132

@@ -2196,3 +2197,252 @@ $converter = new DjotConverter(
21962197
significantNewlines: true,
21972198
);
21982199
```
2200+
2201+
## Social Meta Tags
2202+
2203+
Extract metadata from Djot documents for Open Graph and Twitter Card tags, useful for social sharing previews.
2204+
2205+
### Basic Extraction
2206+
2207+
Extract title, description, and image from a parsed document:
2208+
2209+
```php
2210+
use Djot\DjotConverter;
2211+
use Djot\Node\Block\Heading;
2212+
use Djot\Node\Block\Paragraph;
2213+
use Djot\Node\Document;
2214+
use Djot\Node\Inline\HardBreak;
2215+
use Djot\Node\Inline\Image;
2216+
use Djot\Node\Inline\SoftBreak;
2217+
use Djot\Node\Inline\Text;
2218+
2219+
function extractSocialMeta(Document $document): array
2220+
{
2221+
$meta = [
2222+
'title' => null,
2223+
'description' => null,
2224+
'image' => null,
2225+
];
2226+
2227+
foreach ($document->getChildren() as $node) {
2228+
// First heading becomes title
2229+
if ($meta['title'] === null && $node instanceof Heading) {
2230+
$meta['title'] = getTextContent($node);
2231+
}
2232+
2233+
// First paragraph becomes description
2234+
if ($meta['description'] === null && $node instanceof Paragraph) {
2235+
$text = getTextContent($node);
2236+
$meta['description'] = mb_strlen($text) > 160
2237+
? mb_substr($text, 0, 157) . '...'
2238+
: $text;
2239+
}
2240+
2241+
// First image becomes preview image
2242+
if ($meta['image'] === null) {
2243+
$meta['image'] = findFirstImage($node);
2244+
}
2245+
2246+
// Stop once we have everything
2247+
if ($meta['title'] !== null && $meta['description'] !== null && $meta['image'] !== null) {
2248+
break;
2249+
}
2250+
}
2251+
2252+
return $meta;
2253+
}
2254+
2255+
function getTextContent($node): string
2256+
{
2257+
$text = '';
2258+
foreach ($node->getChildren() as $child) {
2259+
if ($child instanceof Text) {
2260+
$text .= $child->getContent();
2261+
} elseif ($child instanceof SoftBreak || $child instanceof HardBreak) {
2262+
$text .= ' ';
2263+
} elseif (method_exists($child, 'getChildren')) {
2264+
$text .= getTextContent($child);
2265+
}
2266+
}
2267+
return trim($text);
2268+
}
2269+
2270+
function findFirstImage($node): ?string
2271+
{
2272+
if ($node instanceof Image) {
2273+
return $node->getSource();
2274+
}
2275+
if (method_exists($node, 'getChildren')) {
2276+
foreach ($node->getChildren() as $child) {
2277+
$image = findFirstImage($child);
2278+
if ($image !== null) {
2279+
return $image;
2280+
}
2281+
}
2282+
}
2283+
return null;
2284+
}
2285+
2286+
// Usage
2287+
$converter = new DjotConverter();
2288+
$document = $converter->parse($djot);
2289+
$meta = extractSocialMeta($document);
2290+
```
2291+
2292+
### Generating HTML Meta Tags
2293+
2294+
Generate Open Graph and Twitter Card markup:
2295+
2296+
```php
2297+
function generateMetaTags(array $meta, string $url, string $siteName = ''): string
2298+
{
2299+
$tags = [];
2300+
$title = $meta['title'] ?? null;
2301+
$description = $meta['description'] ?? null;
2302+
$image = $meta['image'] ?? null;
2303+
2304+
// Open Graph
2305+
if ($title) {
2306+
$title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
2307+
$tags[] = "<meta property=\"og:title\" content=\"{$title}\">";
2308+
$tags[] = "<meta name=\"twitter:title\" content=\"{$title}\">";
2309+
}
2310+
2311+
if ($description) {
2312+
$desc = htmlspecialchars($description, ENT_QUOTES, 'UTF-8');
2313+
$tags[] = "<meta property=\"og:description\" content=\"{$desc}\">";
2314+
$tags[] = "<meta name=\"twitter:description\" content=\"{$desc}\">";
2315+
$tags[] = "<meta name=\"description\" content=\"{$desc}\">";
2316+
}
2317+
2318+
if ($image) {
2319+
$image = htmlspecialchars($image, ENT_QUOTES, 'UTF-8');
2320+
$tags[] = "<meta property=\"og:image\" content=\"{$image}\">";
2321+
$tags[] = "<meta name=\"twitter:image\" content=\"{$image}\">";
2322+
$tags[] = "<meta name=\"twitter:card\" content=\"summary_large_image\">";
2323+
} else {
2324+
$tags[] = "<meta name=\"twitter:card\" content=\"summary\">";
2325+
}
2326+
2327+
$url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
2328+
$tags[] = "<meta property=\"og:url\" content=\"{$url}\">";
2329+
$tags[] = "<meta property=\"og:type\" content=\"article\">";
2330+
2331+
if ($siteName) {
2332+
$siteName = htmlspecialchars($siteName, ENT_QUOTES, 'UTF-8');
2333+
$tags[] = "<meta property=\"og:site_name\" content=\"{$siteName}\">";
2334+
}
2335+
2336+
return implode("\n", $tags);
2337+
}
2338+
2339+
// Usage
2340+
$meta = extractSocialMeta($document);
2341+
$metaTags = generateMetaTags($meta, 'https://example.com/article', 'My Blog');
2342+
```
2343+
2344+
Output:
2345+
```html
2346+
<meta property="og:title" content="Article Title">
2347+
<meta name="twitter:title" content="Article Title">
2348+
<meta property="og:description" content="First paragraph of the article...">
2349+
<meta name="twitter:description" content="First paragraph of the article...">
2350+
<meta name="description" content="First paragraph of the article...">
2351+
<meta property="og:image" content="https://example.com/image.jpg">
2352+
<meta name="twitter:image" content="https://example.com/image.jpg">
2353+
<meta name="twitter:card" content="summary_large_image">
2354+
<meta property="og:url" content="https://example.com/article">
2355+
<meta property="og:type" content="article">
2356+
<meta property="og:site_name" content="My Blog">
2357+
```
2358+
2359+
### Custom Extraction Rules
2360+
2361+
Override the basic extraction with explicit div attributes:
2362+
2363+
```php
2364+
use Djot\Node\Block\Div;
2365+
use Djot\Node\Document;
2366+
2367+
function extractSocialMetaWithOverrides(Document $document): array
2368+
{
2369+
// Start with basic content extraction
2370+
$meta = extractSocialMeta($document);
2371+
2372+
// Override with explicit div attributes if present
2373+
foreach ($document->getChildren() as $node) {
2374+
if ($node instanceof Div) {
2375+
// Use div attributes: ::: {og-title="Custom Title"}
2376+
if (($ogTitle = $node->getAttribute('og-title')) !== null) {
2377+
$meta['title'] = $ogTitle;
2378+
}
2379+
if (($ogDesc = $node->getAttribute('og-description')) !== null) {
2380+
$meta['description'] = $ogDesc;
2381+
}
2382+
if (($ogImage = $node->getAttribute('og-image')) !== null) {
2383+
$meta['image'] = $ogImage;
2384+
}
2385+
break;
2386+
}
2387+
}
2388+
2389+
return $meta;
2390+
}
2391+
```
2392+
2393+
Usage in Djot:
2394+
```djot
2395+
::: {og-title="Custom Social Title" og-description="A custom description for social sharing"}
2396+
2397+
# Article Title
2398+
2399+
This is the article content...
2400+
2401+
:::
2402+
```
2403+
2404+
### Fallback Values
2405+
2406+
Provide fallbacks for missing metadata:
2407+
2408+
```php
2409+
$meta = extractSocialMeta($document);
2410+
2411+
// Apply fallbacks
2412+
$meta['title'] ??= 'Untitled';
2413+
$meta['description'] ??= 'No description available.';
2414+
$meta['image'] ??= 'https://example.com/default-og-image.jpg';
2415+
2416+
echo generateMetaTags($meta, $currentUrl, 'My Site');
2417+
```
2418+
2419+
### Framework Integration
2420+
2421+
Example controller pattern (adapt `loadArticle()`, `render()`, and `Response` to your framework):
2422+
2423+
```php
2424+
class ArticleController
2425+
{
2426+
public function show(string $slug)
2427+
{
2428+
$djot = $this->loadArticle($slug); // Your article loading logic
2429+
2430+
$converter = new DjotConverter();
2431+
$document = $converter->parse($djot);
2432+
$html = $converter->render($document);
2433+
2434+
$meta = extractSocialMeta($document);
2435+
$metaTags = generateMetaTags(
2436+
$meta,
2437+
"https://example.com/articles/{$slug}",
2438+
'My Blog',
2439+
);
2440+
2441+
return $this->render('article.html', [
2442+
'content' => $html,
2443+
'metaTags' => $metaTags,
2444+
'title' => $meta['title'] ?? 'Article',
2445+
]);
2446+
}
2447+
}
2448+
```

0 commit comments

Comments
 (0)