@@ -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