From bca6f9afc3652cd56b6080869421814550987158 Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Wed, 22 Apr 2026 14:04:51 +0300 Subject: [PATCH 1/2] Adjustments to deal with embeded assets --- .../HtmlHelpers/EntryToHtmlConverter.cs | 14 ++- .../HtmlHelpers/HtmlToRichTextConverter.cs | 92 ++++++++++++++++++- .../HtmlHelpers/RichTextToHtmlConverter.cs | 79 +++++++++++++++- 3 files changed, 181 insertions(+), 4 deletions(-) diff --git a/Apps.Contentful/HtmlHelpers/EntryToHtmlConverter.cs b/Apps.Contentful/HtmlHelpers/EntryToHtmlConverter.cs index 0344e66..a4381e8 100644 --- a/Apps.Contentful/HtmlHelpers/EntryToHtmlConverter.cs +++ b/Apps.Contentful/HtmlHelpers/EntryToHtmlConverter.cs @@ -377,7 +377,19 @@ private string ConvertCustomFieldToHtml(JToken quoteToken) if (content is null) return default; - var richTextToHtmlConverter = new RichTextToHtmlConverter(content, spaceId); + Func? assetResolver = null; + if (includeReferencedAssets) + { + var client = new ContentfulClient(invocationContext.AuthenticationCredentialsProviders, environment); + assetResolver = assetId => + { + var assetTask = client.GetAsset(assetId); + assetTask.Wait(); + return assetTask.Result; + }; + } + + var richTextToHtmlConverter = new RichTextToHtmlConverter(content, spaceId, assetResolver); var fieldContent = richTextToHtmlConverter.ToHtml(); return WrapFieldInDiv(doc, entryId, field, fieldContent); diff --git a/Apps.Contentful/HtmlHelpers/HtmlToRichTextConverter.cs b/Apps.Contentful/HtmlHelpers/HtmlToRichTextConverter.cs index 016e249..7f7047c 100644 --- a/Apps.Contentful/HtmlHelpers/HtmlToRichTextConverter.cs +++ b/Apps.Contentful/HtmlHelpers/HtmlToRichTextConverter.cs @@ -81,6 +81,9 @@ private void ParseHtmlToContentful(HtmlNode node, List contentList) case "a": content = CreateHyperlink(childNode); break; + case "img": + content = CreateAssetFromImage(childNode); + break; default: ParseHtmlToContentful(childNode, contentList); break; @@ -513,6 +516,78 @@ private IContent CreateHyperlink(HtmlNode node) } } + private IContent? CreateAssetFromImage(HtmlNode node) + { + var assetId = node.GetAttributeValue("data-contentful-link-id", ""); + var nodeType = node.GetAttributeValue("data-contentful-rich-text-node-type", ""); + + if (string.IsNullOrEmpty(assetId)) + { + var id = node.GetAttributeValue("id", ""); + if (TryParseRichTextAssetId(id, out nodeType, out assetId) == false) + return null; + } + + if (string.IsNullOrEmpty(nodeType)) + nodeType = "embedded-asset-block"; + + if (nodeType != "embedded-asset-block" && nodeType != "embedded-asset-inline") + return null; + + return new AssetHyperlink + { + NodeType = nodeType, + Content = new List(), + Data = new AssetHyperlinkData + { + Target = new Asset + { + SystemProperties = new SystemProperties + { + Id = assetId, + Type = "Link", + LinkType = "Asset" + } + } + } + }; + } + + private static bool TryParseRichTextAssetId(string id, out string nodeType, out string assetId) + { + nodeType = ""; + assetId = ""; + + const string blockPrefix = "embedded-asset-block_"; + const string inlinePrefix = "embedded-asset-inline_"; + + if (id.StartsWith(blockPrefix)) + { + nodeType = "embedded-asset-block"; + assetId = id[blockPrefix.Length..]; + return !string.IsNullOrEmpty(assetId); + } + + if (id.StartsWith(inlinePrefix)) + { + nodeType = "embedded-asset-inline"; + assetId = id[inlinePrefix.Length..]; + return !string.IsNullOrEmpty(assetId); + } + + return false; + } + + private static string GetAssetNodeTypeFromImage(HtmlNode node) + { + var nodeType = node.GetAttributeValue("data-contentful-rich-text-node-type", ""); + if (!string.IsNullOrEmpty(nodeType)) + return nodeType; + + var id = node.GetAttributeValue("id", ""); + return TryParseRichTextAssetId(id, out nodeType, out _) ? nodeType : "embedded-asset-block"; + } + private string GetHyperlinkTextWithNewlines(HtmlNode node) { var text = new StringBuilder(); @@ -651,6 +726,21 @@ private void GetMarksFromHtmlNode(HtmlNode node, List marks) parentContentList.Add(CreateHyperlink(child)); } } + else if (child.Name == "img") + { + var assetContent = CreateAssetFromImage(child); + if (assetContent == null) + continue; + + if (GetAssetNodeTypeFromImage(child) == "embedded-asset-block") + { + parentContentList.Add(assetContent); + } + else + { + paragraph.Content.Add(assetContent); + } + } else { ParseHtmlToContentful(child, paragraph.Content); @@ -683,4 +773,4 @@ private void GetMarksFromHtmlNode(HtmlNode node, List marks) return paragraph; } -} \ No newline at end of file +} diff --git a/Apps.Contentful/HtmlHelpers/RichTextToHtmlConverter.cs b/Apps.Contentful/HtmlHelpers/RichTextToHtmlConverter.cs index 515ee1b..1c7b214 100644 --- a/Apps.Contentful/HtmlHelpers/RichTextToHtmlConverter.cs +++ b/Apps.Contentful/HtmlHelpers/RichTextToHtmlConverter.cs @@ -1,10 +1,14 @@ using System.Text; +using Contentful.Core.Models.Management; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Apps.Contentful.HtmlHelpers; -public class RichTextToHtmlConverter(JArray content, string spaceId) +public class RichTextToHtmlConverter( + JArray content, + string spaceId, + Func? assetResolver = null) { public string ToHtml() { @@ -80,12 +84,83 @@ private string ConvertJsonObjectToHtml(JObject jsonObject) case "embedded-asset-block": assetId = jsonObject["data"]["target"]["sys"]["id"].ToString(); uri = $"https://app.contentful.com/spaces/{spaceId}/assets/{assetId}"; + var imageHtml = ConvertEmbeddedAssetToImageHtml(assetId, nodeType); + if (imageHtml != null) + { + return imageHtml; + } + return $"Asset {assetId}"; default: return ConvertContentToHtml(jsonObject["content"]); } } + private string? ConvertEmbeddedAssetToImageHtml(string assetId, string nodeType) + { + if (assetResolver == null) + return null; + + ManagementAsset? asset; + try + { + asset = assetResolver(assetId); + } + catch + { + return null; + } + + if (asset?.Files == null) + return null; + + foreach (var fileEntry in asset.Files) + { + var file = fileEntry.Value; + if (!file.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + continue; + + var imageUrl = NormalizeImageUrl(file.Url); + var htmlBuilder = new StringBuilder(); + htmlBuilder.Append($"\"{HtmlEncode(altText)}\"");"); + return htmlBuilder.ToString(); + } + + return null; + } + + private static string NormalizeImageUrl(string imageUrl) + { + if (imageUrl.StartsWith("//")) + { + return "https:" + imageUrl; + } + + if (imageUrl.StartsWith("/")) + { + return "https://images.ctfassets.net" + imageUrl; + } + + return imageUrl; + } + + private static string HtmlEncode(string value) => System.Net.WebUtility.HtmlEncode(value); + private string ConvertHeadingToHtml(JObject jsonObject, string nodeType) { var tagName = nodeType.Replace("heading-", "h"); @@ -221,4 +296,4 @@ private void GetMarksHtml(JToken marks, out string openingMarks, out string clos openingMarks = openingMarksBuilder.ToString(); closingMarks = closingMarksBuilder.ToString(); } -} \ No newline at end of file +} From c9ae2c5febbf45ec9b433a45626752fa4f88a9f4 Mon Sep 17 00:00:00 2001 From: Artem Riabushenko Date: Wed, 22 Apr 2026 14:28:32 +0300 Subject: [PATCH 2/2] Bump version --- Apps.Contentful/Apps.Contentful.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Apps.Contentful/Apps.Contentful.csproj b/Apps.Contentful/Apps.Contentful.csproj index 4ce9523..30ccd84 100644 --- a/Apps.Contentful/Apps.Contentful.csproj +++ b/Apps.Contentful/Apps.Contentful.csproj @@ -6,7 +6,7 @@ enable Contentful The headless content management system - 1.8.13 + 1.8.14 Apps.Contentful