Skip to content

Word-authored hyperlinks lose their URL in PDF output — styled like a link, not clickable #363

Description

@DaveMoudy

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

  1. In Word, select text and use Insert → Link to point it at any URL.
  2. Upload the .docx through the Template Manager and generate as PDF.
  3. 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 &amp; 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdocxRelated to DOCX output or client-side assemblygood first issueGood for newcomershelp wantedExtra attention is neededpdfRelated to PDF output or Blob.toPdf()priority:P1Visible regression or significant bug, has workaroundseverity:silent-corruptionOutput is wrong but no error is raised — highest user impact

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions