Summary
A hyperlink inserted in Word (Insert → Link) renders in the generated PDF with its styling intact — blue, underlined — but is not clickable. The URL is dropped during the DOCX → HTML conversion.
Reported against v3.48.0. Verified still present on main @ ed79216 (v3.56.0).
The reporter had already isolated it correctly: the intermediate HTML contains styled <span> / <u> elements and no <a href="...">. That is exactly what happens, and the code says so out loud at DocGenHtmlRenderer.cls:1950:
// No URL available (template hyperlink without rels) — render as styled text
return '<span style="color:#0563C1;text-decoration:underline">' + innerContent + '</span>';
Mechanism, proven by running it
Word stores a hyperlink as <w:hyperlink r:id="rId5">; the URL itself lives in word/_rels/document.xml.rels keyed by that r:id. processHyperlink only ever emits an <a href> when it finds a w:docgen-url attribute — a custom attribute that DocGenService stamps for rich-text-generated links only (DocGenService.cls:6520-6529). It never resolves r:id, so a Word-authored link always falls through to the <span>.
Both paths run through DocGenHtmlRenderer.convertToHtml on main:
| input |
<a href> in output? |
A. Word-authored — <w:hyperlink r:id="rId5"> |
false |
B. Rich-text — <w:hyperlink w:docgen-url="https://example.com/formX"> |
true, URL preserved |
Case A's actual output, using the reporter's own example text:
<p><span style="color:#0563C1;text-decoration:underline">form X</span></p>
So the renderer is fully capable of emitting a working link. This is a plumbing gap, not a missing capability.
Blob.toPdf does support real PDF links — worth confirming before anyone assumes otherwise
Given Flying Saucer's reputation for silently dropping things (it drops inline <svg>), the obvious worry is that fixing the HTML would not help the PDF. It does. Rendering <a href="https://example.com/formX"> through Blob.toPdf and decoding the bytes:
has /Link subtype : True
has /URI : True
has /Annots : True
URI captured : https://example.com/formX
A real /Link annotation with the correct target. Every component works; only the r:id → URL lookup is missing.
Reproduce
- In Word, select text and use Insert → Link to point it at any URL.
- Upload the
.docx through the Template Manager and generate as PDF.
- The text is blue and underlined, and clicking does nothing.
Assert it directly instead, no template needed:
String xml = '<?xml version="1.0" encoding="UTF-8"?>' +
'<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">' +
'<w:body><w:p><w:hyperlink r:id="rId5" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' +
'<w:r><w:t>form X</w:t></w:r></w:hyperlink></w:p></w:body></w:document>';
System.debug(DocGenHtmlRenderer.convertToHtml(xml).contains('<a href')); // false
Fix options, cheapest first
The rels XML is already loaded and in scope at the call site. DocGenService.cls:1909 is the PDF path:
String html = DocGenHtmlRenderer.convertToHtml(combineXmlWithHeadersFooters(mr), pdfImages);
and mr.relsXml (DocGenService.cls:626) holds word/_rels/document.xml.rels. Nothing new needs to be read or stored.
Option 1 — pre-pass that stamps w:docgen-url (recommended). Before calling convertToHtml, walk <w:hyperlink r:id="rIdN" and add w:docgen-url="<target>" resolved from mr.relsXml. Zero renderer changes — it reuses the docgen-url path already proven to work in case B above. There is already <Relationship> parsing to model on at DocGenService.cls:916-930, written for images. Smallest viable change and the one I'd take.
Option 2 — thread a rels map into the renderer. Add a Map<String, String> relIdToUrl parameter alongside the existing images map and resolve inside processHyperlink. Cleaner separation, but it touches the convertToHtml overload chain (:300, :311, :886) and three signature call sites.
Either way, three details matter:
- Escape the URL for HTML. A query string with
& must become & or the emitted HTML is malformed.
- Honour
TargetMode="External". Internal targets are not URLs.
- Internal bookmarks are a separate case.
<w:hyperlink w:anchor="_Toc123"> has no r:id at all; a cross-reference or TOC link would need href="#..." plus a matching anchor. Out of scope here — worth its own issue if anyone wants in-document navigation.
The existing test codifies the bug
DocGenHtmlRendererTest.testHyperlink:1604 feeds in a r:id hyperlink and asserts only the text, the colour and the underline — never <a href>. It passes against the broken behaviour and will keep passing after a fix, so it is not a regression guard. It needs an <a href> assertion plus a resolved-URL assertion as part of the fix.
Scope
Also affects the signature viewing paths, which call convertToHtml with an image map and no rels: DocGenSignatureSenderController.cls:1596, :1850, :3091. A fix should cover those or state that it doesn't.
Word .docx output is unaffected — this is the DOCX → HTML → PDF path only. A hyperlink in a template generated as .docx keeps its native w:hyperlink relationship.
Priority reasoning
priority:P1 + severity:silent-corruption. It is not literal corruption, but it fits the rubric's "output looks fine but is wrong": an author proof-reading the PDF sees blue underlined link text and has no visual signal that it is dead. The failure surfaces to the recipient, after delivery. Downgrade to severity:visible-regression if that reads as too strong.
Summary
A hyperlink inserted in Word (Insert → Link) renders in the generated PDF with its styling intact — blue, underlined — but is not clickable. The URL is dropped during the DOCX → HTML conversion.
Reported against v3.48.0. Verified still present on
main@ ed79216 (v3.56.0).The reporter had already isolated it correctly: the intermediate HTML contains styled
<span>/<u>elements and no<a href="...">. That is exactly what happens, and the code says so out loud atDocGenHtmlRenderer.cls:1950:Mechanism, proven by running it
Word stores a hyperlink as
<w:hyperlink r:id="rId5">; the URL itself lives inword/_rels/document.xml.relskeyed by thatr:id.processHyperlinkonly ever emits an<a href>when it finds aw:docgen-urlattribute — a custom attribute thatDocGenServicestamps for rich-text-generated links only (DocGenService.cls:6520-6529). It never resolvesr:id, so a Word-authored link always falls through to the<span>.Both paths run through
DocGenHtmlRenderer.convertToHtmlonmain:<a href>in output?<w:hyperlink r:id="rId5"><w:hyperlink w:docgen-url="https://example.com/formX">Case A's actual output, using the reporter's own example text:
So the renderer is fully capable of emitting a working link. This is a plumbing gap, not a missing capability.
Blob.toPdfdoes support real PDF links — worth confirming before anyone assumes otherwiseGiven Flying Saucer's reputation for silently dropping things (it drops inline
<svg>), the obvious worry is that fixing the HTML would not help the PDF. It does. Rendering<a href="https://example.com/formX">throughBlob.toPdfand decoding the bytes:A real
/Linkannotation with the correct target. Every component works; only ther:id→ URL lookup is missing.Reproduce
.docxthrough the Template Manager and generate as PDF.Assert it directly instead, no template needed:
Fix options, cheapest first
The rels XML is already loaded and in scope at the call site.
DocGenService.cls:1909is the PDF path:and
mr.relsXml(DocGenService.cls:626) holdsword/_rels/document.xml.rels. Nothing new needs to be read or stored.Option 1 — pre-pass that stamps
w:docgen-url(recommended). Before callingconvertToHtml, walk<w:hyperlink r:id="rIdN"and addw:docgen-url="<target>"resolved frommr.relsXml. Zero renderer changes — it reuses thedocgen-urlpath already proven to work in case B above. There is already<Relationship>parsing to model on atDocGenService.cls:916-930, written for images. Smallest viable change and the one I'd take.Option 2 — thread a rels map into the renderer. Add a
Map<String, String> relIdToUrlparameter alongside the existingimagesmap and resolve insideprocessHyperlink. Cleaner separation, but it touches theconvertToHtmloverload chain (:300,:311,:886) and three signature call sites.Either way, three details matter:
&must become&or the emitted HTML is malformed.TargetMode="External". Internal targets are not URLs.<w:hyperlink w:anchor="_Toc123">has nor:idat all; a cross-reference or TOC link would needhref="#..."plus a matching anchor. Out of scope here — worth its own issue if anyone wants in-document navigation.The existing test codifies the bug
DocGenHtmlRendererTest.testHyperlink:1604feeds in ar:idhyperlink and asserts only the text, the colour and the underline — never<a href>. It passes against the broken behaviour and will keep passing after a fix, so it is not a regression guard. It needs an<a href>assertion plus a resolved-URL assertion as part of the fix.Scope
Also affects the signature viewing paths, which call
convertToHtmlwith an image map and no rels:DocGenSignatureSenderController.cls:1596,:1850,:3091. A fix should cover those or state that it doesn't.Word
.docxoutput is unaffected — this is the DOCX → HTML → PDF path only. A hyperlink in a template generated as.docxkeeps its nativew:hyperlinkrelationship.Priority reasoning
priority:P1+severity:silent-corruption. It is not literal corruption, but it fits the rubric's "output looks fine but is wrong": an author proof-reading the PDF sees blue underlined link text and has no visual signal that it is dead. The failure surfaces to the recipient, after delivery. Downgrade toseverity:visible-regressionif that reads as too strong.